blob: 970529e1717fd95a0c96dbd4f381c885838621d2 [file] [log] [blame]
package junitparams;
import java.util.ArrayList;
import java.util.List;
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 junitparams.internal.ParameterisedTestClassRunner;
import junitparams.internal.ParametrizedTestMethodsFilter;
import junitparams.internal.TestMethod;
/**
* <h1>JUnitParams</h1><br>
* <p>
* This is a JUnit runner for parameterised tests that don't suck. Annotate your test class with
* <code>&#064;RunWith(JUnitParamsRunner.class)</code> and place
* <code>&#064;Parameters</code> annotation on each test method which requires
* parameters. Nothing more needed - no special structure, no dirty tricks.
* </p>
* <br>
* <h2>Contents</h2> <b> <a href="#p1">1. Parameterising tests</a><br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#a">a. Parameterising tests via values
* in annotation</a><br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#b">b. Parameterising tests via a
* method that returns parameter values</a><br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#c">c. Parameterising tests via
* external classes</a><br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#d">d. Loading parameters from files</a><br>
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#d">e. Converting parameter values</a><br>
* <a href="#p2">2. Usage with Spring</a><br>
* <a href="#p3">3. Other options</a><br>
* </b><br>
* <h3 id="p1">1. Parameterising tests</h3> 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.
*
* <h4 id="a">a. Parameterising tests via values in annotation</h4>
* <p>
* 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 "|".
*
* <pre>
* &#064;Test
* &#064;Parameters({ "20, Tarzan", "0, Jane" })
* public void cartoonCharacters(int yearsInJungle, String person) {
* ...
* }
* </pre>
*
* Sometimes you may be interested in passing enum values as parameters, then
* you can just write them as Strings like this:
*
* <pre>
* &#064;Test
* &#064;Parameters({ &quot;FROM_JUNGLE&quot;, &quot;FROM_CITY&quot; })
* public void passEnumAsParam(PersonType person) {
* }
* </pre>
*
* <h4 id="b">b. Parameterising tests via a method that returns parameter values
* </h4>
* <p>
* 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:
*
* <pre>
* &#064;Test
* &#064;Parameters(method = "cartoonCharacters")
* public void cartoonCharacters(int yearsInJungle, String person) {
* ...
* }
* private Object[] cartoonCharacters() {
* return $(
* $(0, "Tarzan"),
* $(20, "Jane")
* );
* }
* </pre>
*
* Where <code>$(...)</code> is a static method defined in
* <code>JUnitParamsRunner</code> class, which returns its parameters as a
* <code>Object[]</code> array. Just a shortcut, so that you don't need to write the ugly <code>new Object[] {}</code> kind of stuff.
*
* <p>
* <code>method</code> 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.
* <pre>
* &#064;Test
* &#064;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")
* );
* }
* </pre>
* <p>
* The <code>method</code> argument of a <code>@Parameters</code> annotation can
* be ommited if the method that provides parameters has a the same name as the
* test, but prefixed by <code>parametersFor</code>. So our example would look
* like this:
*
* <pre>
* &#064;Test
* &#064;Parameters
* public void cartoonCharacters(int yearsInJungle, String person) {
* ...
* }
* private Object[] parametersForCartoonCharacters() {
* return $(
* $(0, "Tarzan"),
* $(20, "Jane")
* );
* }
* </pre>
*
* <p>
* If you don't like returning untyped values and arrays, you can equally well
* return any Iterable of concrete objects:
*
* <pre>
* &#064;Test
* &#064;Parameters
* public void cartoonCharacters(Person character) {
* ...
* }
* private List&lt;Person&gt; parametersForCartoonCharacters() {
* return Arrays.asList(
* new Person(0, "Tarzan"),
* new Person(20, "Jane")
* );
* }
* </pre>
*
* 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:
*
* <pre>
* &#064;Test
* &#064;Parameters
* public void cartoonCharacters(Person character) {
* ...
* }
* private List&lt;?&gt; parametersForCartoonCharacters() {
* return Arrays.asList(
* $(0, "Tarzan"),
* $(20, "Jane")
* );
* }
* </pre>
* And JUnitParams will invoke the appropriate constructor (<code>new Person(int age, String name)</code> in this case.)
* <b>If you want to use it, watch out! Automatic refactoring of constructor
* arguments won't be working here!</b>
*
* <p>
* 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.
*
* <h4 id="c">c. Parameterising tests via external classes</h4>
* <p>
* 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:
*
* <pre>
* &#064;Test
* &#064;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();
* }
* }
* </pre>
*
* All methods starting with <code>provide</code> are used as parameter
* providers.
*
* <p>
* 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:
*
* <pre>
* &#064;Test
* &#064;Parameters(source = CartoonCharactersProvider.class, method = "cinderellaCharacters,snowwhiteCharacters")
* public void testPrincesses(boolean isAPrincess, String characterName) {
* ...
* }
* </pre>
*
*
* <h4 id="d">d. Loading parameters from files</h4> 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:
*
* <pre>
* &#064;Test
* &#064;FileParameters("cartoon-characters.csv")
* public void shouldSurviveInJungle(int yearsInJungle, String person) {
* ...
* }
* </pre>
*
* 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 <code>IdentityMapper</code>. Look:
*
* <pre>
* &#064;Test
* &#064;FileParameters(value = "cartoon-characters.csv", mapper = CartoonMapper.class)
* public void shouldSurviveInJungle(Person person) {
* ...
* }
*
* public class CartoonMapper extends IdentityMapper {
* &#064;Override
* public Object[] map(Reader reader) {
* Object[] map = super.map(reader);
* List&lt;Object[]&gt; result = new LinkedList&lt;Object[]&gt;();
* 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();
* }
*
* }
* </pre>
*
* A CSV files with a header are also supported with the use of <code>CsvWithHeaderMapper</code> class.
*
* You may also want to use a completely different file format, like excel or
* something. Then just parse it yourself:
*
* <pre>
* &#064;Test
* &#064;FileParameters(value = "cartoon-characters.xsl", mapper = ExcelCartoonMapper.class)
* public void shouldSurviveInJungle(Person person) {
* ...
* }
*
* public class CartoonMapper implements DataMapper {
* &#064;Override
* public Object[] map(Reader fileReader) {
* ...
* }
* }
* </pre>
*
* 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 <code>classpath:</code>
*
* <h4 id="e">e. Converting parameter values</h4>
* 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 <code>&#064;ConvertParam</code> annotation, give it a converter class and possibly some options (like date format in this case) and
* you're done. Here's an example:
* <pre>
* &#064;Test
* &#064;Parameters({ "01.12.2012, A" })
* public void convertMultipleParams(
* &#064;ConvertParam(value = StringToDateConverter.class, options = "dd.MM.yyyy") Date date,
* &#064;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);
* }
* </pre>
*
* <h3 id="p2">2. Usage with Spring</h3>
* <p>
* 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:
*
* <pre>
* private TestContextManager testContextManager;
*
* &#064;Before
* public void init() throws Exception {
* this.testContextManager = new TestContextManager(getClass());
* this.testContextManager.prepareTestInstance(this);
* }
* </pre>
*
* This lets you use in your tests anything that Spring provides in its test
* framework.
*
* <h3 id="p3">3. Other options</h3>
* <h4> Enhancing test case description</h4>
* You can use <code>TestCaseName</code> annotation to provide template of the individual test case name:
* <pre>
* &#064;TestCaseName("factorial({0}) = {1}")
* &#064;Parameters({ "1,1"})
* public void fractional_test(int argument, int result) { }
* </pre>
* Will be displayed as 'fractional(1)=1'
* <h4>Customizing how parameter objects are shown in IDE</h4>
* <p>
* 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 <b>toString</b>
* method for it.
* <h4>Empty parameter sets</h4>
* <p>
* 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.
* <h4>Parameterised test with no parameters</h4>
* <p>
* 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?)
* <h4>JUnit Rules</h4>
* <p>
* 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.
* <h4>Test inheritance</h4>
* <p>
* 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 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);
// Android-changed: Applying a filter could change the description so invalidate any cached
// description. See b/36074730
description = null;
this.parametrizedTestMethodsFilter = new ParametrizedTestMethodsFilter(this,filter);
}
@Override
protected void collectInitializationErrors(List<Throwable> errors) {
super.validateFields(errors);
for (Throwable throwable : errors)
throwable.printStackTrace();
}
@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<Throwable> errors = new ArrayList<Throwable>();
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<FrameworkMethod> 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<FrameworkMethod> resultMethods = getListOfMethods();
for (FrameworkMethod method : resultMethods)
description.addChild(describeMethod(method));
}
return description;
}
private List<FrameworkMethod> getListOfMethods() {
List<FrameworkMethod> 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 <code>Object[]</code> 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
* <code>new Object[]{}</code> instead.
*
* @param params
* Values to be returned in an <code>Object[]</code> array.
* @return Values passed to this method.
*/
@Deprecated
public static Object[] $(Object... params) {
return params;
}
}