Home | History | Annotate | Download | only in robolectric
      1 package org.robolectric;
      2 
      3 import java.lang.annotation.Annotation;
      4 import java.lang.annotation.ElementType;
      5 import java.lang.annotation.Retention;
      6 import java.lang.annotation.RetentionPolicy;
      7 import java.lang.annotation.Target;
      8 import java.lang.reflect.Constructor;
      9 import java.lang.reflect.Field;
     10 import java.lang.reflect.Method;
     11 import java.lang.reflect.Modifier;
     12 import java.text.MessageFormat;
     13 import java.util.ArrayList;
     14 import java.util.Collections;
     15 import java.util.HashSet;
     16 import java.util.List;
     17 import java.util.Locale;
     18 import org.junit.Assert;
     19 import org.junit.runner.Runner;
     20 import org.junit.runners.Parameterized;
     21 import org.junit.runners.Suite;
     22 import org.junit.runners.model.FrameworkField;
     23 import org.junit.runners.model.FrameworkMethod;
     24 import org.junit.runners.model.InitializationError;
     25 import org.junit.runners.model.TestClass;
     26 import org.robolectric.internal.SandboxTestRunner;
     27 import org.robolectric.util.ReflectionHelpers;
     28 
     29 /**
     30  * A Parameterized test runner for Robolectric. Copied from the {@link Parameterized} class, then
     31  * modified the custom test runner to extend the {@link RobolectricTestRunner}. The {@link
     32  * org.robolectric.RobolectricTestRunner#getHelperTestRunner(Class)} is overridden in order to
     33  * create instances of the test class with the appropriate parameters. Merged in the ability to name
     34  * your tests through the {@link Parameters#name()} property. Merged in support for {@link
     35  * Parameter} annotation alternative to providing a constructor.
     36  *
     37  * <p>This class takes care of the fact that the test runner and the test class are actually loaded
     38  * from different class loaders and therefore parameter objects created by one cannot be assigned to
     39  * instances of the other.
     40  */
     41 public final class ParameterizedRobolectricTestRunner extends Suite {
     42 
     43   /**
     44    * Annotation for a method which provides parameters to be injected into the test class
     45    * constructor by <code>Parameterized</code>
     46    */
     47   @Retention(RetentionPolicy.RUNTIME)
     48   @Target(ElementType.METHOD)
     49   public @interface Parameters {
     50 
     51     /**
     52      * Optional pattern to derive the test's name from the parameters. Use numbers in braces to
     53      * refer to the parameters or the additional data as follows:
     54      *
     55      * <pre>
     56      * {index} - the current parameter index
     57      * {0} - the first parameter value
     58      * {1} - the second parameter value
     59      * etc...
     60      * </pre>
     61      *
     62      * <p>Default value is "{index}" for compatibility with previous JUnit versions.
     63      *
     64      * @return {@link MessageFormat} pattern string, except the index placeholder.
     65      * @see MessageFormat
     66      */
     67     String name() default "{index}";
     68   }
     69 
     70   /**
     71    * Annotation for fields of the test class which will be initialized by the method annotated by
     72    * <code>Parameters</code><br>
     73    * By using directly this annotation, the test class constructor isn't needed.<br>
     74    * Index range must start at 0. Default value is 0.
     75    */
     76   @Retention(RetentionPolicy.RUNTIME)
     77   @Target(ElementType.FIELD)
     78   public @interface Parameter {
     79     /**
     80      * Method that returns the index of the parameter in the array returned by the method annotated
     81      * by <code>Parameters</code>.<br>
     82      * Index range must start at 0. Default value is 0.
     83      *
     84      * @return the index of the parameter.
     85      */
     86     int value() default 0;
     87   }
     88 
     89   private static class TestClassRunnerForParameters extends RobolectricTestRunner {
     90 
     91     private final int parametersIndex;
     92     private final String name;
     93 
     94     TestClassRunnerForParameters(Class<?> type, int parametersIndex, String name)
     95         throws InitializationError {
     96       super(type);
     97       this.parametersIndex = parametersIndex;
     98       this.name = name;
     99     }
    100 
    101     private Object createTestInstance(Class bootstrappedClass) throws Exception {
    102       Constructor<?>[] constructors = bootstrappedClass.getConstructors();
    103       Assert.assertEquals(1, constructors.length);
    104       if (!fieldsAreAnnotated()) {
    105         return constructors[0].newInstance(computeParams(bootstrappedClass.getClassLoader()));
    106       } else {
    107         Object instance = constructors[0].newInstance();
    108         injectParametersIntoFields(instance, bootstrappedClass.getClassLoader());
    109         return instance;
    110       }
    111     }
    112 
    113     private Object[] computeParams(ClassLoader classLoader) throws Exception {
    114       // Robolectric uses a different class loader when running the tests, so the parameters objects
    115       // created by the test runner are not compatible with the parameters required by the test.
    116       // Instead, we compute the parameters within the test's class loader.
    117       try {
    118         List<Object[]> parametersList = getParametersList(getTestClass(), classLoader);
    119         if (parametersIndex >= parametersList.size()) {
    120           throw new Exception(
    121               "Re-computing the parameter list returned a different number of "
    122                   + "parameters values. Is the data() method of your test non-deterministic?");
    123         }
    124         return parametersList.get(parametersIndex);
    125       } catch (ClassCastException e) {
    126         throw new Exception(
    127             String.format(
    128                 "%s.%s() must return a Collection of arrays.", getTestClass().getName(), name));
    129       } catch (Exception exception) {
    130         throw exception;
    131       } catch (Throwable throwable) {
    132         throw new Exception(throwable);
    133       }
    134     }
    135 
    136     @SuppressWarnings("unchecked")
    137     private void injectParametersIntoFields(Object testClassInstance, ClassLoader classLoader)
    138         throws Exception {
    139       // Robolectric uses a different class loader when running the tests, so referencing Parameter
    140       // directly causes type mismatches. Instead, we find its class within the test's class loader.
    141       Class<?> parameterClass = getClassInClassLoader(Parameter.class, classLoader);
    142       Object[] parameters = computeParams(classLoader);
    143       HashSet<Integer> parameterFieldsFound = new HashSet<>();
    144       for (Field field : testClassInstance.getClass().getFields()) {
    145         Annotation parameter = field.getAnnotation((Class<Annotation>) parameterClass);
    146         if (parameter != null) {
    147           int index = ReflectionHelpers.callInstanceMethod(parameter, "value");
    148           parameterFieldsFound.add(index);
    149           try {
    150             field.set(testClassInstance, parameters[index]);
    151           } catch (IllegalArgumentException iare) {
    152             throw new Exception(
    153                 getTestClass().getName()
    154                     + ": Trying to set "
    155                     + field.getName()
    156                     + " with the value "
    157                     + parameters[index]
    158                     + " that is not the right type ("
    159                     + parameters[index].getClass().getSimpleName()
    160                     + " instead of "
    161                     + field.getType().getSimpleName()
    162                     + ").",
    163                 iare);
    164           }
    165         }
    166       }
    167       if (parameterFieldsFound.size() != parameters.length) {
    168         throw new IllegalStateException(
    169             String.format(
    170                 Locale.US,
    171                 "Provided %d parameters, but only found fields for parameters: %s",
    172                 parameters.length,
    173                 parameterFieldsFound.toString()));
    174       }
    175     }
    176 
    177     @Override
    178     protected String getName() {
    179       return name;
    180     }
    181 
    182     @Override
    183     protected String testName(final FrameworkMethod method) {
    184       return method.getName() + getName();
    185     }
    186 
    187     @Override
    188     protected void validateConstructor(List<Throwable> errors) {
    189       validateOnlyOneConstructor(errors);
    190       if (fieldsAreAnnotated()) {
    191         validateZeroArgConstructor(errors);
    192       }
    193     }
    194 
    195     @Override
    196     public String toString() {
    197       return "TestClassRunnerForParameters " + name;
    198     }
    199 
    200     @Override
    201     protected void validateFields(List<Throwable> errors) {
    202       super.validateFields(errors);
    203       // Ensure that indexes for parameters are correctly defined
    204       if (fieldsAreAnnotated()) {
    205         List<FrameworkField> annotatedFieldsByParameter = getAnnotatedFieldsByParameter();
    206         int[] usedIndices = new int[annotatedFieldsByParameter.size()];
    207         for (FrameworkField each : annotatedFieldsByParameter) {
    208           int index = each.getField().getAnnotation(Parameter.class).value();
    209           if (index < 0 || index > annotatedFieldsByParameter.size() - 1) {
    210             errors.add(
    211                 new Exception(
    212                     "Invalid @Parameter value: "
    213                         + index
    214                         + ". @Parameter fields counted: "
    215                         + annotatedFieldsByParameter.size()
    216                         + ". Please use an index between 0 and "
    217                         + (annotatedFieldsByParameter.size() - 1)
    218                         + "."));
    219           } else {
    220             usedIndices[index]++;
    221           }
    222         }
    223         for (int index = 0; index < usedIndices.length; index++) {
    224           int numberOfUse = usedIndices[index];
    225           if (numberOfUse == 0) {
    226             errors.add(new Exception("@Parameter(" + index + ") is never used."));
    227           } else if (numberOfUse > 1) {
    228             errors.add(
    229                 new Exception(
    230                     "@Parameter(" + index + ") is used more than once (" + numberOfUse + ")."));
    231           }
    232         }
    233       }
    234     }
    235 
    236     @Override
    237     protected SandboxTestRunner.HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) {
    238       try {
    239         return new HelperTestRunner(bootstrappedTestClass) {
    240           @Override
    241           protected void validateConstructor(List<Throwable> errors) {
    242             TestClassRunnerForParameters.this.validateOnlyOneConstructor(errors);
    243           }
    244 
    245           @Override
    246           protected Object createTest() throws Exception {
    247             return TestClassRunnerForParameters.this.createTestInstance(
    248                 getTestClass().getJavaClass());
    249           }
    250 
    251           @Override
    252           public String toString() {
    253             return "HelperTestRunner for " + TestClassRunnerForParameters.this.toString();
    254           }
    255         };
    256       } catch (InitializationError initializationError) {
    257         throw new RuntimeException(initializationError);
    258       }
    259     }
    260 
    261     private List<FrameworkField> getAnnotatedFieldsByParameter() {
    262       return getTestClass().getAnnotatedFields(Parameter.class);
    263     }
    264 
    265     private boolean fieldsAreAnnotated() {
    266       return !getAnnotatedFieldsByParameter().isEmpty();
    267     }
    268   }
    269 
    270   private final ArrayList<Runner> runners = new ArrayList<>();
    271 
    272   /*
    273    * Only called reflectively. Do not use programmatically.
    274    */
    275   public ParameterizedRobolectricTestRunner(Class<?> klass) throws Throwable {
    276     super(klass, Collections.<Runner>emptyList());
    277     TestClass testClass = getTestClass();
    278     ClassLoader classLoader = getClass().getClassLoader();
    279     Parameters parameters =
    280         getParametersMethod(testClass, classLoader).getAnnotation(Parameters.class);
    281     List<Object[]> parametersList = getParametersList(testClass, classLoader);
    282     for (int i = 0; i < parametersList.size(); i++) {
    283       Object[] parameterArray = parametersList.get(i);
    284       runners.add(
    285           new TestClassRunnerForParameters(
    286               testClass.getJavaClass(), i, nameFor(parameters.name(), i, parameterArray)));
    287     }
    288   }
    289 
    290   @Override
    291   protected List<Runner> getChildren() {
    292     return runners;
    293   }
    294 
    295   @SuppressWarnings("unchecked")
    296   private static List<Object[]> getParametersList(TestClass testClass, ClassLoader classLoader)
    297       throws Throwable {
    298     return (List<Object[]>) getParametersMethod(testClass, classLoader).invokeExplosively(null);
    299   }
    300 
    301   private static FrameworkMethod getParametersMethod(TestClass testClass, ClassLoader classLoader)
    302       throws Exception {
    303     List<FrameworkMethod> methods = testClass.getAnnotatedMethods(Parameters.class);
    304     for (FrameworkMethod each : methods) {
    305       int modifiers = each.getMethod().getModifiers();
    306       if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) {
    307         return getFrameworkMethodInClassLoader(each, classLoader);
    308       }
    309     }
    310 
    311     throw new Exception("No public static parameters method on class " + testClass.getName());
    312   }
    313 
    314   private static String nameFor(String namePattern, int index, Object[] parameters) {
    315     String finalPattern = namePattern.replaceAll("\\{index\\}", Integer.toString(index));
    316     String name = MessageFormat.format(finalPattern, parameters);
    317     return "[" + name + "]";
    318   }
    319 
    320   /**
    321    * Returns the {@link FrameworkMethod} object for the given method in the provided class loader.
    322    */
    323   private static FrameworkMethod getFrameworkMethodInClassLoader(
    324       FrameworkMethod method, ClassLoader classLoader)
    325       throws ClassNotFoundException, NoSuchMethodException {
    326     Method methodInClassLoader = getMethodInClassLoader(method.getMethod(), classLoader);
    327     if (methodInClassLoader.equals(method.getMethod())) {
    328       // The method was already loaded in the right class loader, return it as is.
    329       return method;
    330     }
    331     return new FrameworkMethod(methodInClassLoader);
    332   }
    333 
    334   /** Returns the {@link Method} object for the given method in the provided class loader. */
    335   private static Method getMethodInClassLoader(Method method, ClassLoader classLoader)
    336       throws ClassNotFoundException, NoSuchMethodException {
    337     Class<?> declaringClass = method.getDeclaringClass();
    338 
    339     if (declaringClass.getClassLoader() == classLoader) {
    340       // The method was already loaded in the right class loader, return it as is.
    341       return method;
    342     }
    343 
    344     // Find the class in the class loader corresponding to the declaring class of the method.
    345     Class<?> declaringClassInClassLoader = getClassInClassLoader(declaringClass, classLoader);
    346 
    347     // Find the method with the same signature in the class loader.
    348     return declaringClassInClassLoader.getMethod(method.getName(), method.getParameterTypes());
    349   }
    350 
    351   /** Returns the {@link Class} object for the given class in the provided class loader. */
    352   private static Class<?> getClassInClassLoader(Class<?> klass, ClassLoader classLoader)
    353       throws ClassNotFoundException {
    354     if (klass.getClassLoader() == classLoader) {
    355       // The method was already loaded in the right class loader, return it as is.
    356       return klass;
    357     }
    358 
    359     // Find the class in the class loader corresponding to the declaring class of the method.
    360     return classLoader.loadClass(klass.getName());
    361   }
    362 }
    363