All Downloads are FREE. Search and download functionalities are using the official Maven repository.

junitparams.JUnitParamsRunner Maven / Gradle / Ivy

There is a newer version: 1.1.1
Show newest version
package junitparams;

import java.util.ArrayList;
import java.util.List;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.runner.Description;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;
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 org.junit.validator.PublicClassValidator;

import static org.junit.internal.runners.rules.RuleMemberValidator.*;

import junitparams.internal.ParameterisedTestClassRunner;
import junitparams.internal.ParametrizedTestMethodsFilter;
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 ([email protected]) */ public class JUnitParamsRunner extends BlockJUnit4ClassRunner { private ParametrizedTestMethodsFilter parametrizedTestMethodsFilter = new ParametrizedTestMethodsFilter(this); private ParameterisedTestClassRunner parameterisedRunner; private Description description; public JUnitParamsRunner(Class klass) throws InitializationError { super(klass); parameterisedRunner = new ParameterisedTestClassRunner(getTestClass()); } @Override public void filter(Filter filter) throws NoTestsRemainException { super.filter(filter); this.parametrizedTestMethodsFilter = new ParametrizedTestMethodsFilter(this, filter); } @Override protected void collectInitializationErrors(List errors) { validatePublicTestClass(); validateZeroArgConstructor(errors); validateLifecycleMethods(errors); validateRules(errors); for (Throwable throwable : errors) throwable.printStackTrace(); } private void validatePublicTestClass() { new PublicClassValidator().validateTestClass(getTestClass()); } private void validateRules(List errors) { CLASS_RULE_VALIDATOR.validate(getTestClass(), errors); CLASS_RULE_METHOD_VALIDATOR.validate(getTestClass(), errors); RULE_METHOD_VALIDATOR.validate(getTestClass(), errors); RULE_VALIDATOR.validate(getTestClass(), errors); } private void validateLifecycleMethods(List errors) { validatePublicVoidNoArgMethods(BeforeClass.class, true, errors); validatePublicVoidNoArgMethods(AfterClass.class, true, errors); validatePublicVoidNoArgMethods(After.class, false, errors); validatePublicVoidNoArgMethods(Before.class, false, errors); } @Override protected void runChild(FrameworkMethod method, RunNotifier notifier) { if (handleIgnored(method, notifier)) return; TestMethod testMethod = parameterisedRunner.testMethodFor(method); if (parameterisedRunner.shouldRun(testMethod)) { parameterisedRunner.runParameterisedTest(testMethod, methodBlock(method), notifier); } else { verifyMethodCanBeRunByStandardRunner(testMethod); super.runChild(method, notifier); } } private void verifyMethodCanBeRunByStandardRunner(TestMethod testMethod) { List errors = new ArrayList(); testMethod.frameworkMethod().validatePublicVoidNoArg(false, errors); if (!errors.isEmpty()) { throw new RuntimeException(errors.get(0)); } } private boolean handleIgnored(FrameworkMethod method, RunNotifier notifier) { TestMethod testMethod = parameterisedRunner.testMethodFor(method); if (testMethod.isIgnored()) notifier.fireTestIgnored(describeMethod(method)); return testMethod.isIgnored(); } @Override protected List computeTestMethods() { return parameterisedRunner.computeFrameworkMethods(); } @Override protected Statement methodInvoker(FrameworkMethod method, Object test) { Statement methodInvoker = parameterisedRunner.parameterisedMethodInvoker(method, test); if (methodInvoker == null) methodInvoker = super.methodInvoker(method, test); return methodInvoker; } @Override public Description getDescription() { if (description == null) { description = Description.createSuiteDescription(getName(), getTestClass().getAnnotations()); List resultMethods = getListOfMethods(); for (FrameworkMethod method : resultMethods) description.addChild(describeMethod(method)); } return description; } private List getListOfMethods() { List frameworkMethods = parameterisedRunner.returnListOfMethods(); return parametrizedTestMethodsFilter.filteredMethods(frameworkMethods); } public Description describeMethod(FrameworkMethod method) { Description child = parameterisedRunner.describeParameterisedMethod(method); if (child == null) child = describeChild(method); return child; } /** * 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. * * @param params Values to be returned in an Object[] array. * @return Values passed to this method. * @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. */ @Deprecated public static Object[] $(Object... params) { return params; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy