package org.junit.runners; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.junit.runner.Runner; import org.junit.runner.notification.RunNotifier; import org.junit.runners.model.FrameworkField; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; /** * <p> * The custom runner <code>Parameterized</code> implements parameterized tests. * When running a parameterized test class, instances are created for the * cross-product of the test methods and the test data elements. * </p> * * For example, to test a Fibonacci function, write: * * <pre> * @RunWith(Parameterized.class) * public class FibonacciTest { * @Parameters(name= "{index}: fib({0})={1}") * public static Iterable<Object[]> data() { * return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 }, * { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }); * } * * private int fInput; * * private int fExpected; * * public FibonacciTest(int input, int expected) { * fInput= input; * fExpected= expected; * } * * @Test * public void test() { * assertEquals(fExpected, Fibonacci.compute(fInput)); * } * } * </pre> * * <p> * Each instance of <code>FibonacciTest</code> will be constructed using the * two-argument constructor and the data values in the * <code>@Parameters</code> method. * * <p> * In order that you can easily identify the individual tests, you may provide a * name for the <code>@Parameters</code> annotation. This name is allowed * to contain placeholders, which are replaced at runtime. The placeholders are * <dl> * <dt>{index}</dt> * <dd>the current parameter index</dd> * <dt>{0}</dt> * <dd>the first parameter value</dd> * <dt>{1}</dt> * <dd>the second parameter value</dd> * <dt>...</dt> * <dd></dd> * </dl> * In the example given above, the <code>Parameterized</code> runner creates * names like <code>[1: fib(3)=2]</code>. If you don't use the name parameter, * then the current parameter index is used as name. * </p> * * You can also write: * * <pre> * @RunWith(Parameterized.class) * public class FibonacciTest { * @Parameters * public static Iterable<Object[]> data() { * return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 }, * { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }); * } * @Parameter(0) * public int fInput; * * @Parameter(1) * public int fExpected; * * @Test * public void test() { * assertEquals(fExpected, Fibonacci.compute(fInput)); * } * } * </pre> * * <p> * Each instance of <code>FibonacciTest</code> will be constructed with the default constructor * and fields annotated by <code>@Parameter</code> will be initialized * with the data values in the <code>@Parameters</code> method. * </p> * * @since 4.0 */ public class Parameterized extends Suite { /** * Annotation for a method which provides parameters to be injected into the * test class constructor by <code>Parameterized</code> */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public static @interface Parameters { /** * <p> * Optional pattern to derive the test's name from the parameters. Use * numbers in braces to refer to the parameters or the additional data * as follows: * </p> * * <pre> * {index} - the current parameter index * {0} - the first parameter value * {1} - the second parameter value * etc... * </pre> * <p> * Default value is "{index}" for compatibility with previous JUnit * versions. * </p> * * @return {@link MessageFormat} pattern string, except the index * placeholder. * @see MessageFormat */ String name() default "{index}"; } /** * Annotation for fields of the test class which will be initialized by the * method annotated by <code>Parameters</code><br/> * By using directly this annotation, the test class constructor isn't needed.<br/> * Index range must start at 0. * Default value is 0. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public static @interface Parameter { /** * Method that returns the index of the parameter in the array * returned by the method annotated by <code>Parameters</code>.<br/> * Index range must start at 0. * Default value is 0. * * @return the index of the parameter. */ int value() default 0; } private class TestClassRunnerForParameters extends BlockJUnit4ClassRunner { private final Object[] fParameters; private final String fName; TestClassRunnerForParameters(Class<?> type, Object[] parameters, String name) throws InitializationError { super(type); fParameters = parameters; fName = name; } @Override public Object createTest() throws Exception { if (fieldsAreAnnotated()) { return createTestUsingFieldInjection(); } else { return createTestUsingConstructorInjection(); } } private Object createTestUsingConstructorInjection() throws Exception { return getTestClass().getOnlyConstructor().newInstance(fParameters); } private Object createTestUsingFieldInjection() throws Exception { List<FrameworkField> annotatedFieldsByParameter = getAnnotatedFieldsByParameter(); if (annotatedFieldsByParameter.size() != fParameters.length) { throw new Exception("Wrong number of parameters and @Parameter fields." + " @Parameter fields counted: " + annotatedFieldsByParameter.size() + ", available parameters: " + fParameters.length + "."); } Object testClassInstance = getTestClass().getJavaClass().newInstance(); for (FrameworkField each : annotatedFieldsByParameter) { Field field = each.getField(); Parameter annotation = field.getAnnotation(Parameter.class); int index = annotation.value(); try { field.set(testClassInstance, fParameters[index]); } catch (IllegalArgumentException iare) { throw new Exception(getTestClass().getName() + ": Trying to set " + field.getName() + " with the value " + fParameters[index] + " that is not the right type (" + fParameters[index].getClass().getSimpleName() + " instead of " + field.getType().getSimpleName() + ").", iare); } } return testClassInstance; } @Override protected String getName() { return fName; } @Override protected String testName(FrameworkMethod method) { return method.getName() + getName(); } @Override protected void validateConstructor(List<Throwable> errors) { validateOnlyOneConstructor(errors); if (fieldsAreAnnotated()) { validateZeroArgConstructor(errors); } } @Override protected void validateFields(List<Throwable> errors) { super.validateFields(errors); if (fieldsAreAnnotated()) { List<FrameworkField> annotatedFieldsByParameter = getAnnotatedFieldsByParameter(); int[] usedIndices = new int[annotatedFieldsByParameter.size()]; for (FrameworkField each : annotatedFieldsByParameter) { int index = each.getField().getAnnotation(Parameter.class).value(); if (index < 0 || index > annotatedFieldsByParameter.size() - 1) { errors.add( new Exception("Invalid @Parameter value: " + index + ". @Parameter fields counted: " + annotatedFieldsByParameter.size() + ". Please use an index between 0 and " + (annotatedFieldsByParameter.size() - 1) + ".") ); } else { usedIndices[index]++; } } for (int index = 0; index < usedIndices.length; index++) { int numberOfUse = usedIndices[index]; if (numberOfUse == 0) { errors.add(new Exception("@Parameter(" + index + ") is never used.")); } else if (numberOfUse > 1) { errors.add(new Exception("@Parameter(" + index + ") is used more than once (" + numberOfUse + ").")); } } } } @Override protected Statement classBlock(RunNotifier notifier) { return childrenInvoker(notifier); } @Override protected Annotation[] getRunnerAnnotations() { return new Annotation[0]; } } private static final List<Runner> NO_RUNNERS = Collections .<Runner>emptyList(); private final ArrayList<Runner> runners = new ArrayList<Runner>(); /** * Only called reflectively. Do not use programmatically. */ public Parameterized(Class<?> klass) throws Throwable { super(klass, NO_RUNNERS); Parameters parameters = getParametersMethod().getAnnotation( Parameters.class); createRunnersForParameters(allParameters(), parameters.name()); } @Override protected List<Runner> getChildren() { return runners; } @SuppressWarnings("unchecked") private Iterable<Object[]> allParameters() throws Throwable { Object parameters = getParametersMethod().invokeExplosively(null); if (parameters instanceof Iterable) { return (Iterable<Object[]>) parameters; } else { throw parametersMethodReturnedWrongType(); } } private FrameworkMethod getParametersMethod() throws Exception { List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods( Parameters.class); for (FrameworkMethod each : methods) { if (each.isStatic() && each.isPublic()) { return each; } } throw new Exception("No public static parameters method on class " + getTestClass().getName()); } private void createRunnersForParameters(Iterable<Object[]> allParameters, String namePattern) throws InitializationError, Exception { try { int i = 0; for (Object[] parametersOfSingleTest : allParameters) { String name = nameFor(namePattern, i, parametersOfSingleTest); TestClassRunnerForParameters runner = new TestClassRunnerForParameters( getTestClass().getJavaClass(), parametersOfSingleTest, name); runners.add(runner); ++i; } } catch (ClassCastException e) { throw parametersMethodReturnedWrongType(); } } private String nameFor(String namePattern, int index, Object[] parameters) { String finalPattern = namePattern.replaceAll("\\{index\\}", Integer.toString(index)); String name = MessageFormat.format(finalPattern, parameters); return "[" + name + "]"; } private Exception parametersMethodReturnedWrongType() throws Exception { String className = getTestClass().getName(); String methodName = getParametersMethod().getName(); String message = MessageFormat.format( "{0}.{1}() must return an Iterable of arrays.", className, methodName); return new Exception(message); } private List<FrameworkField> getAnnotatedFieldsByParameter() { return getTestClass().getAnnotatedFields(Parameter.class); } private boolean fieldsAreAnnotated() { return !getAnnotatedFieldsByParameter().isEmpty(); } }