1package org.robolectric; 2 3import java.lang.annotation.ElementType; 4import java.lang.annotation.Retention; 5import java.lang.annotation.RetentionPolicy; 6import java.lang.annotation.Target; 7import java.lang.reflect.Constructor; 8import java.lang.reflect.Modifier; 9import java.text.MessageFormat; 10import java.util.ArrayList; 11import java.util.Collections; 12import java.util.List; 13import javax.annotation.Nonnull; 14import org.junit.Assert; 15import org.junit.runner.Runner; 16import org.junit.runners.Parameterized; 17import org.junit.runners.Suite; 18import org.junit.runners.model.FrameworkMethod; 19import org.junit.runners.model.InitializationError; 20import org.junit.runners.model.TestClass; 21import org.robolectric.internal.DeepCloner; 22import org.robolectric.internal.SandboxTestRunner; 23import org.robolectric.internal.SdkEnvironment; 24 25/** 26 * A Parameterized test runner for Robolectric. Copied from the {@link Parameterized} class, then modified the custom 27 * test runner to extend the {@link RobolectricTestRunner}. The {@link RobolectricTestRunner#getHelperTestRunner(Class)} 28 * is overridden in order to create instances of the test class with the appropriate parameters. Merged in the ability 29 * to name your tests through the {@link Parameters#name()} property. 30 */ 31public final class ParameterizedRobolectricTestRunner extends Suite { 32 33 /** 34 * Annotation for a method which provides parameters to be injected into the test class constructor by 35 * {@code Parameterized} 36 */ 37 @Retention(RetentionPolicy.RUNTIME) 38 @Target(ElementType.METHOD) 39 public @interface Parameters { 40 41 /** 42 * Optional pattern to derive the test's name from the parameters. Use numbers in braces to refer to the 43 * parameters or the additional data as follows: 44 * 45 * <pre> 46 * {index} - the current parameter index 47 * {0} - the first parameter value 48 * {1} - the second parameter value 49 * etc... 50 * </pre> 51 * 52 * Default value is "{index}" for compatibility with previous JUnit versions. 53 * 54 * @return {@link MessageFormat} pattern string, except the index placeholder. 55 * @see MessageFormat 56 */ 57 String name() default "{index}"; 58 } 59 60 private static class TestClassRunnerForParameters extends RobolectricTestRunner { 61 62 private final String name; 63 private Object[] parameters; 64 65 TestClassRunnerForParameters(Class<?> type, Object[] parameters, String name) throws InitializationError { 66 super(type); 67 this.parameters = parameters; 68 this.name = name; 69 } 70 71 private Object createTestInstance(Class bootstrappedClass) throws Exception { 72 Constructor<?>[] constructors = bootstrappedClass.getConstructors(); 73 Assert.assertEquals(1, constructors.length); 74 return constructors[0].newInstance(computeParams()); 75 } 76 77 private Object[] computeParams() throws Exception { 78 try { 79 return parameters; 80 } catch (ClassCastException e) { 81 throw new Exception(String.format("%s.%s() must return a Collection of arrays.", 82 getTestClass().getName(), 83 name)); 84 } 85 } 86 87 @Override 88 protected String getName() { 89 return name; 90 } 91 92 @Override 93 protected String testName(final FrameworkMethod method) { 94 return method.getName() + getName(); 95 } 96 97 @Override 98 protected void validateConstructor(List<Throwable> errors) { 99 validateOnlyOneConstructor(errors); 100 } 101 102 @Nonnull 103 @Override 104 protected SdkEnvironment getSandbox(FrameworkMethod method) { 105 SdkEnvironment sandbox = super.getSandbox(method); 106 107 DeepCloner deepCloner = new DeepCloner(sandbox.getRobolectricClassLoader()); 108 parameters = deepCloner.clone(parameters); 109 110 return sandbox; 111 } 112 113 @Override 114 public String toString() { 115 return "TestClassRunnerForParameters " + name; 116 } 117 118 @Override 119 protected SandboxTestRunner.HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) { 120 try { 121 return new HelperTestRunner(bootstrappedTestClass) { 122 @Override 123 protected void validateConstructor(List<Throwable> errors) { 124 TestClassRunnerForParameters.this.validateOnlyOneConstructor(errors); 125 } 126 127 @Override 128 protected Object createTest() throws Exception { 129 return TestClassRunnerForParameters.this.createTestInstance(getTestClass().getJavaClass()); 130 } 131 132 @Override 133 public String toString() { 134 return "HelperTestRunner for " + TestClassRunnerForParameters.this.toString(); 135 } 136 }; 137 } catch (InitializationError initializationError) { 138 throw new RuntimeException(initializationError); 139 } 140 } 141 } 142 143 private final ArrayList<Runner> runners = new ArrayList<>(); 144 145 /* 146 * Only called reflectively. Do not use programmatically. 147 */ 148 public ParameterizedRobolectricTestRunner(Class<?> klass) throws Throwable { 149 super(klass, Collections.<Runner>emptyList()); 150 Parameters parameters = getParametersMethod().getAnnotation(Parameters.class); 151 List<Object[]> parametersList = getParametersList(); 152 for (int i = 0; i < parametersList.size(); i++) { 153 Object[] parameterArray = parametersList.get(i); 154 runners.add(new TestClassRunnerForParameters(getTestClass().getJavaClass(), 155 parameterArray, 156 nameFor(parameters.name(), i, parameterArray))); 157 } 158 } 159 160 @Override 161 protected List<Runner> getChildren() { 162 return runners; 163 } 164 165 @SuppressWarnings("unchecked") 166 private List<Object[]> getParametersList() throws Throwable { 167 return (List<Object[]>) getParametersMethod().invokeExplosively(null); 168 } 169 170 private FrameworkMethod getParametersMethod() throws Exception { 171 TestClass testClass = getTestClass(); 172 List<FrameworkMethod> methods = testClass.getAnnotatedMethods(Parameters.class); 173 for (FrameworkMethod each : methods) { 174 int modifiers = each.getMethod().getModifiers(); 175 if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) { 176 return each; 177 } 178 } 179 180 throw new Exception("No public static parameters method on class " + testClass.getName()); 181 } 182 183 private static String nameFor(String namePattern, int index, Object[] parameters) { 184 String finalPattern = namePattern.replaceAll("\\{index\\}", Integer.toString(index)); 185 String name = MessageFormat.format(finalPattern, parameters); 186 return "[" + name + "]"; 187 } 188 189} 190