Skip to content

Instantly share code, notes, and snippets.

@christopherperry
Created July 27, 2012 22:02
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christopherperry/3190683 to your computer and use it in GitHub Desktop.
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.
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;
}
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();
}
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();
}
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();
}
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();
}
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());
}
}
}
}
}
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";
}
}
}
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;
}
}
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