Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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