package junitparams; import java.util.List; import junitparams.internal.MethodBlockSupplier; import org.junit.runner.Description; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import junitparams.internal.DescribableFrameworkMethod; import junitparams.internal.InstanceFrameworkMethod; import junitparams.internal.InvokableFrameworkMethod; import junitparams.internal.NonParameterisedFrameworkMethod; import junitparams.internal.ParameterisedFrameworkMethod; import junitparams.internal.TestMethod; /** *

JUnitParams


*

* This is a JUnit runner for parameterised tests that don't suck. Annotate your test class with * @RunWith(JUnitParamsRunner.class) and place * @Parameters annotation on each test method which requires * parameters. Nothing more needed - no special structure, no dirty tricks. *

*
*

Contents

1. Parameterising tests
*      a. Parameterising tests via values * in annotation
*      b. Parameterising tests via a * method that returns parameter values
*      c. Parameterising tests via * external classes
*      d. Loading parameters from files
*      e. Converting parameter values
* 2. Usage with Spring
* 3. Other options
*

*

1. Parameterising tests

Parameterised tests are a great way * to limit the amount of test code when you need to test the same code under * different conditions. Ever tried to do it with standard JUnit tools like * Parameterized runner or Theories? I always thought they're so awkward to use, * that I've written this library to help all those out there who'd like to have * a handy tool. * * So here we go. There are a few different ways to use JUnitParams, I will try * to show you all of them here. * *

a. Parameterising tests via values in annotation

*

* You can parameterise your test with values defined in annotations. Just pass * sets of test method argument values as an array of Strings, where each string * contains the argument values separated by a comma or a pipe "|". * *

 *   @Test
 *   @Parameters({ "20, Tarzan", "0, Jane" })
 *   public void cartoonCharacters(int yearsInJungle, String person) {
 *       ...
 *   }
 * 
* * Sometimes you may be interested in passing enum values as parameters, then * you can just write them as Strings like this: * *
 * @Test
 * @Parameters({ "FROM_JUNGLE", "FROM_CITY" })
 * public void passEnumAsParam(PersonType person) {
 * }
 * 
* *

b. Parameterising tests via a method that returns parameter values *

*

* Obviously passing parameters as strings is handy only for trivial situations, * that's why for normal cases you have a method that gives you a collection of * parameters: * *

 *   @Test
 *   @Parameters(method = "cartoonCharacters")
 *   public void cartoonCharacters(int yearsInJungle, String person) {
 *       ...
 *   }
 *   private Object[] cartoonCharacters() {
 *      return $(
 *          $(0, "Tarzan"),
 *          $(20, "Jane")
 *      );
 *   }
 * 
* * Where $(...) is a static method defined in * JUnitParamsRunner class, which returns its parameters as a * Object[] array. Just a shortcut, so that you don't need to write the ugly new Object[] {} kind of stuff. * *

* method can take more than one method name - you can pass as many * of them as you want, separated by commas. This enables you to divide your * test cases e.g. into categories. *

 *   @Test
 *   @Parameters(method = "menCharactes, womenCharacters")
 *   public void cartoonCharacters(int yearsInJungle, String person) {
 *       ...
 *   }
 *   private Object[] menCharacters() {
 *      return $(
 *          $(20, "Tarzan"),
 *          $(2, "Chip"),
 *          $(2, "Dale")
 *      );
 *   }
 *   private Object[] womenCharacters() {
 *      return $(
 *          $(0, "Jane"),
 *          $(18, "Pocahontas")
 *      );
 *   }
 * 
*

* The method argument of a @Parameters annotation can * be ommited if the method that provides parameters has a the same name as the * test, but prefixed by parametersFor. So our example would look * like this: * *

 *   @Test
 *   @Parameters
 *   public void cartoonCharacters(int yearsInJungle, String person) {
 *       ...
 *   }
 *   private Object[] parametersForCartoonCharacters() {
 *      return $(
 *          $(0, "Tarzan"),
 *          $(20, "Jane")
 *      );
 *   }
 * 
* *

* If you don't like returning untyped values and arrays, you can equally well * return any Iterable of concrete objects: * *

 *   @Test
 *   @Parameters
 *   public void cartoonCharacters(Person character) {
 *       ...
 *   }
 *   private List<Person> parametersForCartoonCharacters() {
 *      return Arrays.asList(
 *          new Person(0, "Tarzan"),
 *          new Person(20, "Jane")
 *      );
 *   }
 * 
* * If we had more than just two Person's to make, we would get redundant, * so JUnitParams gives you a simplified way of creating objects to be passed as * params. You can omit the creation of the objects and just return their constructor * argument values like this: * *
 *   @Test
 *   @Parameters
 *   public void cartoonCharacters(Person character) {
 *       ...
 *   }
 *   private List<?> parametersForCartoonCharacters() {
 *      return Arrays.asList(
 *          $(0, "Tarzan"),
 *          $(20, "Jane")
 *      );
 *   }
 * 
* And JUnitParams will invoke the appropriate constructor (new Person(int age, String name) in this case.) * If you want to use it, watch out! Automatic refactoring of constructor * arguments won't be working here! * *

* You can also define methods that provide parameters in subclasses and use * them in test methods defined in superclasses, as well as redefine data * providing methods in subclasses to be used by test method defined in a * superclass. That you can doesn't mean you should. Inheritance in tests is * usually a code smell (readability hurts), so make sure you know what you're * doing. * *

c. Parameterising tests via external classes

*

* For more complex cases you may want to externalise the method that provides * parameters or use more than one method to provide parameters to a single test * method. You can easily do that like this: * *

 *   @Test
 *   @Parameters(source = CartoonCharactersProvider.class)
 *   public void testReadyToLiveInJungle(int yearsInJungle, String person) {
 *       ...
 *   }
 *   ...
 *   class CartoonCharactersProvider {
 *      public static Object[] provideCartoonCharactersManually() {
 *          return $(
 *              $(0, "Tarzan"),
 *              $(20, "Jane")
 *          );
 *      }
 *      public static Object[] provideCartoonCharactersFromDB() {
 *          return cartoonsRepository.loadCharacters();
 *      }
 *   }
 * 
* * All methods starting with provide are used as parameter * providers. * *

* Sometimes though you may want to use just one or few methods of some class to * provide you parameters. This can be done as well like this: * *

 *   @Test
 *   @Parameters(source = CartoonCharactersProvider.class, method = "cinderellaCharacters,snowwhiteCharacters")
 *   public void testPrincesses(boolean isAPrincess, String characterName) {
 *       ...
 *   }
 * 
* * *

d. Loading parameters from files

You may be interested in * loading parameters from a file. This is very easy if it's a CSV file with * columns in the same order as test method parameters: * *
 *   @Test
 *   @FileParameters("cartoon-characters.csv")
 *   public void shouldSurviveInJungle(int yearsInJungle, String person) {
 *       ...
 *   }
 * 
* * But if you want to process the data from the CSV file a bit to use it in the * test method arguments, you * need to use an IdentityMapper. Look: * *
 *   @Test
 *   @FileParameters(value = "cartoon-characters.csv", mapper = CartoonMapper.class)
 *   public void shouldSurviveInJungle(Person person) {
 *       ...
 *   }
 *
 *   public class CartoonMapper extends IdentityMapper {
 *     @Override
 *     public Object[] map(Reader reader) {
 *         Object[] map = super.map(reader);
 *         List<Object[]> result = new LinkedList<Object[]>();
 *         for (Object lineObj : map) {
 *             String line = (String) lineObj; // line in a format just like in the file
 *             result.add(new Object[] { ..... }); // some format edible by the test method
 *         }
 *         return result.toArray();
 *     }
 *
 * }
 * 
* * A CSV files with a header are also supported with the use of CsvWithHeaderMapper class. * * You may also want to use a completely different file format, like excel or * something. Then just parse it yourself: * *
 *   @Test
 *   @FileParameters(value = "cartoon-characters.xsl", mapper = ExcelCartoonMapper.class)
 *   public void shouldSurviveInJungle(Person person) {
 *       ...
 *   }
 *
 *   public class CartoonMapper implements DataMapper {
 *     @Override
 *     public Object[] map(Reader fileReader) {
 *         ...
 *     }
 * }
 * 
* * As you see, you don't need to open or close the file. Just read it from the * reader and parse it the way you wish. * * By default the file is loaded from the file system, relatively to where you start the tests from. But you can also use a resource from * the classpath by prefixing the file name with classpath: * *

e. Converting parameter values

* Sometimes you want to pass some parameter in one form, but use it in the test in another. Dates are a good example. It's handy to * specify them in the parameters as a String like "2013.01.01", but you'd like to use a Jodatime's LocalDate or JDKs Date in the test * without manually converting the value in the test. This is where the converters become handy. It's enough to annotate a parameter with * a @ConvertParam annotation, give it a converter class and possibly some options (like date format in this case) and * you're done. Here's an example: *
 *     @Test
 *     @Parameters({ "01.12.2012, A" })
 *     public void convertMultipleParams(
 *                  @ConvertParam(value = StringToDateConverter.class, options = "dd.MM.yyyy") Date date,
 *                  @ConvertParam(LetterToASCIIConverter.class) int num) {
 *
 *         Calendar calendar = Calendar.getInstance();
 *         calendar.setTime(date);
 *
 *         assertEquals(2012, calendar.get(Calendar.YEAR));
 *         assertEquals(11, calendar.get(Calendar.MONTH));
 *         assertEquals(1, calendar.get(Calendar.DAY_OF_MONTH));
 *
 *         assertEquals(65, num);
 *     }
 * 
* *

2. Usage with Spring

*

* You can easily use JUnitParams together with Spring. The only problem is that * Spring's test framework is based on JUnit runners, and JUnit allows only one * runner to be run at once. Which would normally mean that you could use only * one of Spring or JUnitParams. Luckily we can cheat Spring a little by adding * this to your test class: * *

 * private TestContextManager testContextManager;
 *
 * @Before
 * public void init() throws Exception {
 *     this.testContextManager = new TestContextManager(getClass());
 *     this.testContextManager.prepareTestInstance(this);
 * }
 * 
* * This lets you use in your tests anything that Spring provides in its test * framework. * *

3. Other options

*

Enhancing test case description

* You can use TestCaseName annotation to provide template of the individual test case name: *
 *     @TestCaseName("factorial({0}) = {1}")
 *     @Parameters({ "1,1"})
 *     public void fractional_test(int argument, int result) { }
 * 
* Will be displayed as 'fractional(1)=1' *

Customizing how parameter objects are shown in IDE

*

* Tests show up in your IDE as a tree with test class name being the root, test * methods being nodes, and parameter sets being the leaves. If you want to * customize the way an parameter object is shown, create a toString * method for it. *

Empty parameter sets

*

* If you create a parameterised test, but won't give it any parameter sets, it * will be ignored and you'll be warned about it. *

Parameterised test with no parameters

*

* If for some reason you want to have a normal non-parameterised method to be * annotated with @Parameters, then fine, you can do it. But it will be ignored * then, since there won't be any params for it, and parameterised tests need * parameters to execute properly (parameters are a part of test setup, right?) *

JUnit Rules

*

* The runner for parameterised test is trying to keep all the @Rule's running, * but if something doesn't work - let me know. It's pretty tricky, since the * rules in JUnit are chained, but the chain is kind of... unstructured, so * sometimes I need to guess how to call the next element in chain. If you have * your own rule, make sure it has a field of type Statement which is the next * statement in chain to call. *

Test inheritance

*

* Although usually a bad idea, since it makes tests less readable, sometimes * inheritance is the best way to remove repetitions from tests. JUnitParams is * fine with inheritance - you can define a common test in the superclass, and * have separate parameters provider methods in the subclasses. Also the other * way around is ok, you can define parameter providers in superclass and have * tests in subclasses uses them as their input. * * @author Pawel Lipinski (lipinski.pawel@gmail.com) */ public class JUnitParamsRunner extends BlockJUnit4ClassRunner { private final MethodBlockSupplier methodBlockSupplier; public JUnitParamsRunner(Class klass) throws InitializationError { super(klass); methodBlockSupplier = new MethodBlockSupplier() { @Override public Statement getMethodBlock(InvokableFrameworkMethod method) { return methodBlock(method); } }; } @Override protected void collectInitializationErrors(List errors) { super.validateFields(errors); for (Throwable throwable : errors) throwable.printStackTrace(); } @Override protected void runChild(FrameworkMethod method, RunNotifier notifier) { DescribableFrameworkMethod describableMethod = getDescribableMethod(method); if (handleIgnored(describableMethod, notifier)) return; if (method instanceof ParameterisedFrameworkMethod) { ParameterisedFrameworkMethod parameterisedFrameworkMethod = (ParameterisedFrameworkMethod) method; List methods = parameterisedFrameworkMethod.getMethods(); for (InstanceFrameworkMethod frameworkMethod : methods) { frameworkMethod.run(methodBlockSupplier, notifier); } } else if (describableMethod instanceof InvokableFrameworkMethod) { ((InvokableFrameworkMethod) describableMethod).run(methodBlockSupplier, notifier); } else { throw new IllegalStateException( "Unsupported FrameworkMethod class: " + method.getClass()); } } /** * Check that the supplied method is one that was originally in the list returned by * {@link #computeTestMethods()}. * * @param method the method, must be an instance of {@link DescribableFrameworkMethod} * @return the supplied method cast to {@link DescribableFrameworkMethod} * @throws IllegalArgumentException if the supplied method is not a * {@link DescribableFrameworkMethod} */ private DescribableFrameworkMethod getDescribableMethod(FrameworkMethod method) { if (!(method instanceof DescribableFrameworkMethod)) { throw new IllegalArgumentException( "Unsupported FrameworkMethod class: " + method.getClass() + ", expected a DescribableFrameworkMethod subclass"); } return (DescribableFrameworkMethod) method; } private boolean handleIgnored(DescribableFrameworkMethod method, RunNotifier notifier) { // A parameterised method that is ignored (either due to @Ignore or due to empty parameters) // is treated as if it was a non-parameterised method. boolean ignored = (method instanceof NonParameterisedFrameworkMethod) && ((NonParameterisedFrameworkMethod) method).isIgnored(); if (ignored) notifier.fireTestIgnored(method.getDescription()); return ignored; } @Override protected List computeTestMethods() { return TestMethod.listFrom(getTestClass()); } @Override protected Statement methodInvoker(FrameworkMethod method, Object test) { if (method instanceof InvokableFrameworkMethod) { return ((InvokableFrameworkMethod) method).getInvokeStatement(test); } throw new IllegalStateException( "Unsupported FrameworkMethod class: " + method.getClass() + ", expected an InvokableFrameworkMethod subclass"); } @Override protected Description describeChild(FrameworkMethod method) { return getDescribableMethod(method).getDescription(); } /** * Shortcut for returning an array of objects. All parameters passed to this * method are returned in an Object[] array. * * Should not be used to create var-args arrays, because of the way Java resolves * var-args for objects and primitives. * * @deprecated This method is no longer supported. It might be removed in future * as it does not support all cases (especially var-args). Create arrays using * new Object[]{} instead. * * @param params * Values to be returned in an Object[] array. * @return Values passed to this method. */ @Deprecated public static Object[] $(Object... params) { return params; } }