Created
July 27, 2012 22:02
-
-
Save christopherperry/3190683 to your computer and use it in GitHub Desktop.
Robolectric test runner with Guice injection, custom class binding using annotations, and android version support.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package annotations; | |
import android.os.Build; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
/** | |
* Changes the Android version Robolectric reports to your code. | |
* Allows you to test logic flows based on Android version. | |
* | |
* @author Christopher J. Perry {github.com/christopherperry} | |
*/ | |
@Target(ElementType.METHOD) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface AndroidVersion { | |
int value() default Build.VERSION_CODES.HONEYCOMB; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package annotations; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
/** | |
* Binds one class to another. | |
* | |
* @author Christopher J. Perry {github.com/christopherperry} | |
*/ | |
@Target(ElementType.METHOD) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface Bind { | |
Class<?> from(); | |
Class<?> to(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package annotations; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
/** | |
* Binds the specified ModuleWrapper for the test. | |
* Use this to override the default module in the test runner. | |
* | |
* @author Christopher J. Perry {github.com/christopherperry} | |
*/ | |
@Target(ElementType.TYPE) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface BindModule { | |
Class<? extends ModuleWrapper> value(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package annotations; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
/** | |
* Binds multiple classes. | |
* | |
* @author Christopher J. Perry {github.com/christopherperry} | |
*/ | |
@Target(ElementType.METHOD) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface BindMultiple { | |
Class<?>[] from(); | |
Class<?>[] to(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package annotations; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
@Target(ElementType.TYPE) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface LoadsLibraryResources { | |
String filePath(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.app.Application; | |
import android.os.Build; | |
import annotations.*; | |
import com.google.inject.AbstractModule; | |
import com.google.inject.Injector; | |
import com.google.inject.Singleton; | |
import com.google.inject.util.Modules; | |
import com.squareup.otto.Bus; | |
import com.xtremelabs.robolectric.Robolectric; | |
import com.xtremelabs.robolectric.RobolectricTestRunner; | |
import org.junit.runners.model.InitializationError; | |
import roboguice.RoboGuice; | |
import java.io.File; | |
import java.lang.annotation.Annotation; | |
import java.lang.reflect.Constructor; | |
import java.lang.reflect.Method; | |
import java.lang.reflect.Modifier; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.Map; | |
import java.util.Set; | |
import static com.xtremelabs.robolectric.Robolectric.shadowOf; | |
/** | |
* A test runner that gives you dependency injection via RoboGuice, | |
* as well as handles custom annotations applied to test methods. | |
*/ | |
public class RobolectricTestRunnerWithInjection extends RobolectricTestRunner { | |
private static final int SDK_INT = Build.VERSION.SDK_INT; | |
private ModuleWrapper moduleWrapper = new TestRunnerModule(); | |
public RobolectricTestRunnerWithInjection(Class<?> testClass) throws InitializationError { | |
super(testClass); | |
} | |
@Override | |
final public void beforeTest(Method method) { | |
/** | |
* Method annotations | |
*/ | |
Annotation[] methodAnnotations = method.getAnnotations(); | |
for (Annotation annotation : methodAnnotations) { | |
Class<? extends Annotation> annotationClass = annotation.annotationType(); | |
if (AndroidVersion.class == annotationClass) { | |
setAndroidVersion((AndroidVersion) annotation); | |
} | |
if (Bind.class == annotationClass) { | |
Bind bind = (Bind) annotation; | |
moduleWrapper.bind(bind.from(), bind.to()); | |
} | |
if (BindMultiple.class == annotationClass) { | |
BindMultiple bindMultiple = (BindMultiple) annotation; | |
Class<?>[] from = bindMultiple.from(); | |
Class<?>[] to = bindMultiple.to(); | |
for (int i = 0; i < from.length; i++) { | |
moduleWrapper.bind(from[i], to[i]); | |
} | |
} | |
if (UsesScreenSize.class == annotationClass) { | |
UsesScreenSize usesScreenSize = (UsesScreenSize)annotation; | |
try { | |
ScreenSize screenSize = usesScreenSize.value(); | |
Robolectric.getShadowApplication().getResourceLoader().setLayoutQualifierSearchPath(screenSize.getPath()); | |
Robolectric.getShadowApplication().getResources().getConfiguration().screenLayout = screenSize.getConfigurationSize(); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
} | |
@Override | |
final public void prepareTest(Object test) { | |
Annotation[] classAnnotations = test.getClass().getAnnotations(); | |
for (Annotation annotation : classAnnotations) { | |
Class<? extends Annotation> annotationClass = annotation.annotationType(); | |
if (LoadsLibraryResources.class == annotationClass) { | |
LoadsLibraryResources loadsLibraryResources = (LoadsLibraryResources) annotation; | |
try { | |
Robolectric.getShadowApplication().getResourceLoader().loadLibraryProjectResources(new File(loadsLibraryResources.filePath())); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
if (BindModule.class == annotationClass) { | |
BindModule bindModule = (BindModule) annotation; | |
try { | |
Class<? extends ModuleWrapper> theClass = bindModule.value(); | |
ModuleWrapper theOldBinder = moduleWrapper; | |
ModuleWrapper theNewBinder; | |
boolean isInnerClass = theClass.isMemberClass(); | |
boolean isStaticClass = Modifier.isStatic(theClass.getModifiers()); | |
if (isInnerClass && !isStaticClass) { | |
Constructor<? extends ModuleWrapper> theClassConstructor = | |
theClass.getConstructor(theClass.getEnclosingClass()); | |
theNewBinder = theClassConstructor.newInstance(test); | |
} else { | |
Constructor<? extends ModuleWrapper> theClassConstructor = theClass.getConstructor(); | |
theNewBinder = theClassConstructor.newInstance(); | |
} | |
theNewBinder.configure(); | |
theNewBinder.overrideBindings(theOldBinder); | |
moduleWrapper = theNewBinder; | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} | |
Application injectedApplication = Robolectric.application; | |
Injector injector = RoboGuice.getInjector(injectedApplication); | |
RoboGuice.setBaseApplicationInjector(injectedApplication, RoboGuice.DEFAULT_STAGE, | |
Modules.override(RoboGuice.newDefaultRoboModule(injectedApplication)).with(moduleWrapper.getModule())); | |
injector.injectMembers(test); | |
} | |
@Override | |
final public void afterTest(Method method) { | |
resetStaticState(); | |
moduleWrapper.clearBindings(); | |
} | |
@Override | |
protected void resetStaticState() { | |
setStaticValue(Build.VERSION.class, "SDK_INT", SDK_INT); | |
} | |
private void setAndroidVersion(AndroidVersion androidVersion) { | |
final int targetSdkVersion = androidVersion.value(); | |
setStaticValue(Build.VERSION.class, "SDK_INT", targetSdkVersion); | |
} | |
private class TestRunnerModule extends ModuleWrapper { | |
@Override | |
public void configure() { | |
// No modules bound in default implementation | |
} | |
} | |
public static abstract class ModuleWrapper { | |
private Map<Class, Class> classToClassBindings = new HashMap<Class, Class>(); | |
private Map<Class, Object> classToInstanceBindings = new HashMap<Class, Object>(); | |
private AbstractModule module = new WrappedModule(); | |
final public AbstractModule getModule() { | |
return module; | |
} | |
final public void clearBindings() { | |
classToClassBindings.clear(); | |
classToInstanceBindings.clear(); | |
} | |
// TODO: make a binding builder so this reads more natural | |
final public void bind(Class from, Class to) { | |
classToClassBindings.put(from, to); | |
} | |
// TODO: make a binding builder so this reads more natural | |
final public void bind(Class from, Object toInstance) { | |
classToInstanceBindings.put(from, toInstance); | |
} | |
final public void overrideBindings(ModuleWrapper that) { | |
/** | |
* Remove previously bound keys from both lists, so | |
* we can properly override the bindings, or we might | |
* end up with duplicates | |
*/ | |
Set<Class> classKeys = new HashSet<Class>(); | |
classKeys.addAll(that.classToClassBindings.keySet()); | |
classKeys.addAll(that.classToInstanceBindings.keySet()); | |
classToClassBindings.keySet().removeAll(classKeys); | |
classToInstanceBindings.keySet().removeAll(classKeys); | |
// Now add everything we're overriding | |
classToClassBindings.putAll(that.classToClassBindings); | |
classToInstanceBindings.putAll(that.classToInstanceBindings); | |
} | |
public abstract void configure(); | |
private class WrappedModule extends AbstractModule { | |
@Override | |
final protected void configure() { | |
for (Map.Entry<Class, Class> entry : classToClassBindings.entrySet()) { | |
bind(entry.getKey()).to(entry.getValue()); | |
} | |
for (Map.Entry<Class, Object> entry : classToInstanceBindings.entrySet()) { | |
bind(entry.getKey()).toInstance(entry.getValue()); | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context; | |
import android.os.Bundle; | |
import android.view.LayoutInflater; | |
import annotations.Bind; | |
import annotations.BindModule; | |
import com.google.inject.Inject; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import roboguice.activity.RoboActivity; | |
import static com.pivotallabs.robolectricgem.expect.Expect.expect; | |
/** | |
* Tests for RobolectricTestRunnerWithInjection. | |
* | |
* @author Christopher J. Perry {github.com/christopherperry} | |
*/ | |
@RunWith(RobolectricTestRunnerWithInjection.class) | |
@BindModule(RobolectricTestRunnerWithInjectionTest.InnerTestModule.class) | |
public class RobolectricTestRunnerWithInjectionTest { | |
@Inject private Context context; | |
@Inject private LayoutInflater inflater; | |
private TestClassFive testClassFive = new TestClassFive(); | |
public class InnerTestModule extends ModuleWrapper { | |
@Override | |
public void configure() { | |
bind(TestClassOne.class, TestClassTwo.class); | |
bind(TestClassFour.class, testClassFive); | |
} | |
} | |
@Test | |
public void bindModuleAnnotation_onClassDeclaration_shouldBeUsedCorrectly() { | |
TestActivityWithInjection myActivity = new TestActivityWithInjection(); | |
myActivity.onCreate(null); | |
TestClassOne classOne = myActivity.getClassOne(); | |
expect(classOne.value()).toEqual("two"); | |
} | |
@Test | |
public void shouldInjectContext() { | |
expect(context).not.toBeNull(); | |
} | |
@Test | |
public void shouldInjectLayoutInflater() { | |
expect(inflater).not.toBeNull(); | |
} | |
@Test | |
public void shouldInjectContextIntoClass() { | |
TestActivityWithInjection myActivity = new TestActivityWithInjection(); | |
myActivity.onCreate(null); | |
expect(myActivity.getContext()).not.toBeNull(); | |
} | |
@Test | |
public void shouldInjectInflaterIntoClass() { | |
TestActivityWithInjection myActivity = new TestActivityWithInjection(); | |
myActivity.onCreate(null); | |
expect(myActivity.getInflater()).not.toBeNull(); | |
} | |
@Test | |
public void shouldUseBindAnnotationCorrectlyForApplicationClasses() { | |
TestActivityWithInjection myActivity = new TestActivityWithInjection(); | |
myActivity.onCreate(null); | |
TestClassOne classOne = myActivity.getClassOne(); | |
expect(classOne.value()).toEqual("two"); | |
} | |
@Test | |
@Bind(from = TestClassOne.class, to = TestClassThree.class) | |
public void bindAnnotation_onMethodDeclaration_shouldOverrideModuleBinding_onClassDeclaration() { | |
TestActivityWithInjection myActivity = new TestActivityWithInjection(); | |
myActivity.onCreate(null); | |
TestClassOne classOne = myActivity.getClassOne(); | |
expect(classOne.value()).toEqual("three"); | |
} | |
@Test | |
@Bind(from = TestClassFour.class, to = TestClassSix.class) | |
public void bindAnnotation_onMethodDeclaration_shouldOverrideModuleBindingWithInstance_onClassDeclaration() { | |
TestActivityWithInjection myActivity = new TestActivityWithInjection(); | |
myActivity.onCreate(null); | |
TestClassFour classFour = myActivity.getClassFour(); | |
expect(classFour.value()).toEqual("six"); | |
} | |
private static class TestActivityWithInjection extends RoboActivity { | |
@Inject private Context context; | |
@Inject private LayoutInflater inflater; | |
@Inject private TestClassOne classOne; | |
@Inject private TestClassFour classFour; | |
@Override | |
public void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
} | |
public TestClassOne getClassOne() { | |
return classOne; | |
} | |
public TestClassFour getClassFour() { | |
return classFour; | |
} | |
public LayoutInflater getInflater() { | |
return inflater; | |
} | |
public Context getContext() { | |
return context; | |
} | |
} | |
private static class TestClassOne { | |
public String value() { | |
return "one"; | |
} | |
} | |
private static class TestClassTwo extends TestClassOne { | |
@Override | |
public String value() { | |
return "two"; | |
} | |
} | |
private static class TestClassThree extends TestClassOne { | |
@Override | |
public String value() { | |
return "three"; | |
} | |
} | |
private static class TestClassFour { | |
public String value() { | |
return "four"; | |
} | |
} | |
private static class TestClassFive extends TestClassFour { | |
@Override | |
public String value() { | |
return "five"; | |
} | |
} | |
private static class TestClassSix extends TestClassFour { | |
@Override | |
public String value() { | |
return "six"; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package annotations; | |
import android.content.res.Configuration; | |
public enum ScreenSize { | |
SMALL("small", Configuration.SCREENLAYOUT_SIZE_SMALL), | |
NORMAL("normal", Configuration.SCREENLAYOUT_SIZE_NORMAL), | |
LARGE("large", Configuration.SCREENLAYOUT_SIZE_LARGE), | |
XLARGE("xlarge", Configuration.SCREENLAYOUT_SIZE_XLARGE); | |
private String path; | |
private int configurationSize; | |
private ScreenSize(String path, int configurationSize) { | |
this.path = path; | |
this.configurationSize = configurationSize; | |
} | |
public String getPath() { | |
return path; | |
} | |
public int getConfigurationSize() { | |
return configurationSize; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package annotations; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
@Target(ElementType.METHOD) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface UsesScreenSize { | |
ScreenSize value() default ScreenSize.NORMAL; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment