diff options
Diffstat (limited to 'src/main/java/junitparams')
40 files changed, 2633 insertions, 0 deletions
diff --git a/src/main/java/junitparams/FileParameters.java b/src/main/java/junitparams/FileParameters.java new file mode 100644 index 0000000..1649b58 --- /dev/null +++ b/src/main/java/junitparams/FileParameters.java @@ -0,0 +1,43 @@ +package junitparams; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import junitparams.custom.CustomParameters; +import junitparams.custom.FileParametersProvider; +import junitparams.mappers.DataMapper; +import junitparams.mappers.IdentityMapper; + +/** + * Denotes that parameters for a annotated test method should be taken from an + * external resource. + * + * @author Pawel Lipinski + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@CustomParameters(provider = FileParametersProvider.class) +public @interface FileParameters { + + /** + * File name (with full path) of the file with data. + */ + String value(); + + /** + * The mapper which knows how to get the data from the external resource and + * turn it into a valid set of parameters. By default it is an + * IdentityMapper, meaning the resource has exactly the same format as the + * <p/> + * @Parameters annotation value (when passed as String), being CSV. + */ + Class<? extends DataMapper> mapper() default IdentityMapper.class; + + /** + * Encoding to use when reading file contents. + */ + String encoding() default "UTF-8"; + +} diff --git a/src/main/java/junitparams/JUnitParamsRunner.java b/src/main/java/junitparams/JUnitParamsRunner.java new file mode 100644 index 0000000..fa37257 --- /dev/null +++ b/src/main/java/junitparams/JUnitParamsRunner.java @@ -0,0 +1,501 @@ +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>@RunWith(JUnitParamsRunner.class)</code> and place + * <code>@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> + * <a href="#a">a. Parameterising tests via values + * in annotation</a><br> + * <a href="#b">b. Parameterising tests via a + * method that returns parameter values</a><br> + * <a href="#c">c. Parameterising tests via + * external classes</a><br> + * <a href="#d">d. Loading parameters from files</a><br> + * <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> + * @Test + * @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> + * @Test + * @Parameters({ "FROM_JUNGLE", "FROM_CITY" }) + * 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> + * @Test + * @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> + * @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") + * ); + * } + * </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> + * @Test + * @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> + * @Test + * @Parameters + * public void cartoonCharacters(Person character) { + * ... + * } + * private List<Person> 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> + * @Test + * @Parameters + * public void cartoonCharacters(Person character) { + * ... + * } + * private List<?> 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> + * @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(); + * } + * } + * </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> + * @Test + * @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> + * @Test + * @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> + * @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(); + * } + * + * } + * </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> + * @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) { + * ... + * } + * } + * </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>@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> + * @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); + * } + * </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; + * + * @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> + * @TestCaseName("factorial({0}) = {1}") + * @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); + 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; + } +} diff --git a/src/main/java/junitparams/Parameters.java b/src/main/java/junitparams/Parameters.java new file mode 100644 index 0000000..2a7b21a --- /dev/null +++ b/src/main/java/junitparams/Parameters.java @@ -0,0 +1,49 @@ +package junitparams; + +import javax.lang.model.type.NullType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * THE annotation for the test parameters. Use it to say that a method takes + * some parameters and define how to obtain them. + * + * @author Pawel Lipinski + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Parameters { + /** + * Parameter values defined as a String array. Each element in the array is + * a full parameter set, comma-separated or pipe-separated ('|'). + * The values must match the method parameters in order and type. + * Whitespace characters are trimmed (use source class or method if You need to provide such parameters) + * + * Example: <code>@Parameters({ + * "1, joe, 26.4, true", + * "2, angie, 37.2, false"})</code> + */ + String[] value() default {}; + + /** + * Parameter values defined externally. The specified class must have at + * least one public static method starting with <code>provide</code> + * returning <code>Object[]</code>. All such methods are used, so you can + * group your examples. The resulting array should contain parameter sets in + * its elements. Each parameter set must be another Object[] array, which + * contains parameter values in its elements. + * Example: <code>@Parameters(source = PeopleProvider.class)</code> + */ + Class<?> source() default NullType.class; + + /** + * Parameter values returned by a method within the test class. This way you + * don't need additional classes and the test code may be a bit cleaner. The + * format of the data returned by the method is the same as for the source + * annotation class. + * Example: <code>@Parameters(method = "examplaryPeople")</code> + * + * You can use multiple methods to provide parameters - use comma to do it: + * Example: <code>@Parameters(method = "womenParams, menParams")</code> + */ + String method() default ""; +} diff --git a/src/main/java/junitparams/converters/ConversionFailedException.java b/src/main/java/junitparams/converters/ConversionFailedException.java new file mode 100644 index 0000000..2a96c0c --- /dev/null +++ b/src/main/java/junitparams/converters/ConversionFailedException.java @@ -0,0 +1,7 @@ +package junitparams.converters; + +public class ConversionFailedException extends Exception { + public ConversionFailedException(String message) { + super(message); + } +} diff --git a/src/main/java/junitparams/converters/ConvertParam.java b/src/main/java/junitparams/converters/ConvertParam.java new file mode 100644 index 0000000..f452943 --- /dev/null +++ b/src/main/java/junitparams/converters/ConvertParam.java @@ -0,0 +1,25 @@ +package junitparams.converters; + +import java.lang.annotation.*; + +/** + * + * Defines a converter which should be used to convert a parameter to expected + * type. + * + * @deprecated use {@link Param} + * @author Pawel Lipinski + */ + +@Retention(RetentionPolicy.RUNTIME) +@Deprecated +public @interface ConvertParam { + + Class<? extends ParamConverter<?>> value(); + + /** + * Options / settings to be used by the converter class + */ + String options() default ""; + +} diff --git a/src/main/java/junitparams/converters/Converter.java b/src/main/java/junitparams/converters/Converter.java new file mode 100644 index 0000000..1ef9c1b --- /dev/null +++ b/src/main/java/junitparams/converters/Converter.java @@ -0,0 +1,26 @@ +package junitparams.converters; + +import java.lang.annotation.Annotation; + +/** + * Defines the logic to convert parameter annotated with A to type T. Converter must have a public no-args constructor. Configuration is + * done via {@link Converter#initialize(java.lang.annotation.Annotation)} method<br> + * Inspired by javax.validation.ConstraintValidator + * + * @param <A> type of annotation mentioning this converter + * @param <T> conversion target type + */ +public interface Converter<A extends Annotation, T> { + + /** + * Initializes this converter - you can read your annotation config here. + */ + void initialize(A annotation); + + /** + * Converts param to desired type. + * + * @throws ConversionFailedException + */ + T convert(Object param) throws ConversionFailedException; +} diff --git a/src/main/java/junitparams/converters/Nullable.java b/src/main/java/junitparams/converters/Nullable.java new file mode 100644 index 0000000..c959e9a --- /dev/null +++ b/src/main/java/junitparams/converters/Nullable.java @@ -0,0 +1,36 @@ + +package junitparams.converters; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import junitparams.Parameters; + +/** + * Allows test null values defined as a String array in {@link Parameters} + * + * @author Peter Jurkovic + * + * <p> + * Example: + * <pre> + * {@literal @}Test + * {@literal @}Parameters({" null "}) + * public void shouldBeNull({@literal @}Nullable String value) { + * assertThat(value).isNull(); + * } + * </pre> + * </p> + */ +@Retention(RetentionPolicy.RUNTIME) +@Param(converter = NullableConverter.class) +@Target({ElementType.ANNOTATION_TYPE, ElementType.PARAMETER}) +public @interface Nullable { + + /** + * Defines parameter value which will be replaced by Java null + */ + String nullIdentifier() default "null"; +} diff --git a/src/main/java/junitparams/converters/NullableConverter.java b/src/main/java/junitparams/converters/NullableConverter.java new file mode 100644 index 0000000..98e9038 --- /dev/null +++ b/src/main/java/junitparams/converters/NullableConverter.java @@ -0,0 +1,19 @@ +package junitparams.converters; + + +public class NullableConverter implements Converter<Nullable, String>{ + + private String nullIdentifier; + + public void initialize(Nullable annotation) { + nullIdentifier = annotation.nullIdentifier() == null ? "null" : annotation.nullIdentifier(); + } + + public String convert(Object param) throws ConversionFailedException { + if(param instanceof String && ((String)param).trim().equalsIgnoreCase(nullIdentifier)){ + return null; + } + return (String)param; + } + +}
\ No newline at end of file diff --git a/src/main/java/junitparams/converters/Param.java b/src/main/java/junitparams/converters/Param.java new file mode 100644 index 0000000..e8ab65d --- /dev/null +++ b/src/main/java/junitparams/converters/Param.java @@ -0,0 +1,29 @@ +package junitparams.converters; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates parametrized test parameter with information about {@link Converter} that should be used for parameter conversion. + * <p> + * Can also be used to create custom annotations.<br> + * example: + * <pre> + * @Retention(RetentionPolicy.RUNTIME) + * @Target(ElementType.PARAMETER) + * @Param(converter = FormattedDateConverter.class) + * public @interface DateParam { + * + * String format() default "dd.MM.yyyy"; + * } + * </pre> + * </p> + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.ANNOTATION_TYPE, ElementType.PARAMETER}) +public @interface Param { + + Class<? extends Converter> converter(); +} diff --git a/src/main/java/junitparams/converters/ParamAnnotation.java b/src/main/java/junitparams/converters/ParamAnnotation.java new file mode 100644 index 0000000..0aca51e --- /dev/null +++ b/src/main/java/junitparams/converters/ParamAnnotation.java @@ -0,0 +1,32 @@ +package junitparams.converters; + +import java.lang.annotation.Annotation; + +public class ParamAnnotation { + + public static boolean matches(Annotation annotation) { + return getParam(annotation) != null; + } + + public static Object convert(Annotation annotation, Object param) throws ConversionFailedException { + return converter(annotation).convert(param); + } + + private static Param getParam(Annotation annotation) { + if (annotation.annotationType().isAssignableFrom(Param.class)) { + return (Param) annotation; + } + return annotation.annotationType().getAnnotation(Param.class); + } + + private static Converter converter(Annotation annotation) { + Converter converter = null; + try { + converter = getParam(annotation).converter().newInstance(); + } catch (Exception e) { + throw new RuntimeException("Your Converter class must have a public no-arg constructor!", e); + } + converter.initialize(annotation); + return converter; + } +} diff --git a/src/main/java/junitparams/converters/ParamConverter.java b/src/main/java/junitparams/converters/ParamConverter.java new file mode 100644 index 0000000..4080ff0 --- /dev/null +++ b/src/main/java/junitparams/converters/ParamConverter.java @@ -0,0 +1,16 @@ +package junitparams.converters; + +/** + * + * Implement this interface if you want to convert params from some + * representation to the type expected by your test method's parameter. + * + * <T> is the expected parameter type. + * + * @deprecated use {@link Converter} + * @author Pawel Lipinski + */ +@Deprecated +public interface ParamConverter<T> { + T convert(Object param, String options) throws ConversionFailedException; +} diff --git a/src/main/java/junitparams/custom/CustomParameters.java b/src/main/java/junitparams/custom/CustomParameters.java new file mode 100644 index 0000000..06fd4a4 --- /dev/null +++ b/src/main/java/junitparams/custom/CustomParameters.java @@ -0,0 +1,25 @@ +package junitparams.custom; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Tells JUnitParams which {@link ParametersProvider} to use for parameters generation.<br> + * Use instead of {@link junitparams.Parameters} annotation. + * <p> + * Can also be used to create custom annotations.<br> + * Check {@link junitparams.FileParameters}, {@link FileParametersProvider} and CustomParametersProviderTest for usage examples. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface CustomParameters { + + /** + * @return Your custom parameters provider class. + */ + Class<? extends ParametersProvider> provider(); + +} diff --git a/src/main/java/junitparams/custom/FileParametersProvider.java b/src/main/java/junitparams/custom/FileParametersProvider.java new file mode 100644 index 0000000..746fe5c --- /dev/null +++ b/src/main/java/junitparams/custom/FileParametersProvider.java @@ -0,0 +1,62 @@ +package junitparams.custom; + +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; + +import junitparams.FileParameters; +import junitparams.mappers.DataMapper; + +public class FileParametersProvider implements ParametersProvider<FileParameters> { + + private FileParameters fileParameters; + + @Override + public void initialize(FileParameters fileParameters) { + this.fileParameters = fileParameters; + } + + @Override + public Object[] getParameters() { + return paramsFromFile(); + } + + private Object[] paramsFromFile() { + try { + Reader reader = createProperReader(); + DataMapper mapper = fileParameters.mapper().newInstance(); + try { + return mapper.map(reader); + } finally { + reader.close(); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException( + "Could not successfully read parameters from file: " + fileParameters.value(), e); + } + } + + private Reader createProperReader() throws IOException { + String filepath = fileParameters.value(); + String encoding = fileParameters.encoding(); + + if (filepath.indexOf(':') < 0) { + return new FileReader(filepath); + } + + String protocol = filepath.substring(0, filepath.indexOf(':')); + String filename = filepath.substring(filepath.indexOf(':') + 1); + + if ("classpath".equals(protocol)) { + return new InputStreamReader(getClass().getClassLoader().getResourceAsStream(filename), encoding); + } else if ("file".equals(protocol)) { + return new InputStreamReader(new FileInputStream(filename), encoding); + } + + throw new IllegalArgumentException("Unknown file access protocol. Only 'file' and 'classpath' are supported!"); + } + +} diff --git a/src/main/java/junitparams/custom/ParametersProvider.java b/src/main/java/junitparams/custom/ParametersProvider.java new file mode 100644 index 0000000..8cf3621 --- /dev/null +++ b/src/main/java/junitparams/custom/ParametersProvider.java @@ -0,0 +1,26 @@ +package junitparams.custom; + +import java.lang.annotation.Annotation; + +/** + * An interface for custom parameters providers. To be used with {@link CustomParameters} annotation. + * Must have a default no-args constructor. + * + * @param <A> type of annotation mentioning this provider + */ +public interface ParametersProvider<A extends Annotation> { + + /** + * Initializes this provider - you can read your custom annotation config here. + * + * @param parametersAnnotation parameters annotation on test method + */ + void initialize(A parametersAnnotation); + + /** + * Actual parameters generation + * + * @return parameters for test method calls + */ + Object[] getParameters(); +} diff --git a/src/main/java/junitparams/custom/combined/Cartesian.java b/src/main/java/junitparams/custom/combined/Cartesian.java new file mode 100644 index 0000000..f98948f --- /dev/null +++ b/src/main/java/junitparams/custom/combined/Cartesian.java @@ -0,0 +1,46 @@ +package junitparams.custom.combined; + +import java.util.Arrays; +import java.util.List; + +class Cartesian { + + static Object[] getCartesianProductOf(List<Object[]> array) { + if (array == null || array.size() == 0) { + return new Object[]{}; + } + + for (int i = 0; i < array.size() - 1; i++) { + Object[] arrayOne = array.get(i); + Object[] arrayTwo = array.get(i + 1); + array.set(i + 1, cartesianProduct(arrayOne, arrayTwo)); + } + + return array.get(array.size() - 1); + } + + private static Object[] cartesianProduct(Object[] arrayOne, Object[] arrayTwo) { + int numberOfCombinations = arrayOne.length * arrayTwo.length; + Object[] resultArray = new Object[numberOfCombinations][2]; + + int i = 0; + for (Object firstElement : arrayOne) { + for (Object secondElement : arrayTwo) { + resultArray[i] = getCartesianOfTwoElements(firstElement, secondElement); + i++; + } + } + + return resultArray; + } + + private static Object getCartesianOfTwoElements(Object objectOne, Object objectTwo) { + if (!objectOne.getClass().isArray()) { + return new Object[]{objectOne, objectTwo}; + } + Object[] initialArray = (Object[]) objectOne; + Object[] newArray = Arrays.copyOf(initialArray, initialArray.length + 1); + newArray[newArray.length - 1] = objectTwo; + return newArray; + } +} diff --git a/src/main/java/junitparams/custom/combined/CombinedParameters.java b/src/main/java/junitparams/custom/combined/CombinedParameters.java new file mode 100644 index 0000000..95e4a5e --- /dev/null +++ b/src/main/java/junitparams/custom/combined/CombinedParameters.java @@ -0,0 +1,24 @@ +package junitparams.custom.combined; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import junitparams.custom.CustomParameters; + +@Retention(RetentionPolicy.RUNTIME) +@CustomParameters(provider = CombinedParametersProvider.class) +public @interface CombinedParameters { + /** + * Parameter values defined as a String array. + * Each of the elements is a list of values that should be tested for parameters. + * Using this annotation will result in creating a n-fold cartesian product of parameter values effectively testing + * each possible combination. + * Values in the array must match the test method's parameters in order and type. + * <p> + * Example:<br> + * <code>@CombinedParameters({"han,chewie","33,204"})<br> + * public void shouldTestAllNameAgeCombinations(String name, Integer age) + * </code> + */ + String[] value() default {}; +} diff --git a/src/main/java/junitparams/custom/combined/CombinedParametersProvider.java b/src/main/java/junitparams/custom/combined/CombinedParametersProvider.java new file mode 100644 index 0000000..382b6ce --- /dev/null +++ b/src/main/java/junitparams/custom/combined/CombinedParametersProvider.java @@ -0,0 +1,27 @@ +package junitparams.custom.combined; + +import java.util.ArrayList; +import java.util.List; + +import junitparams.custom.ParametersProvider; +import junitparams.internal.Utils; + +public class CombinedParametersProvider implements ParametersProvider<CombinedParameters> { + + private CombinedParameters combinedParameters; + + @Override + public void initialize(CombinedParameters parametersAnnotation) { + this.combinedParameters = parametersAnnotation; + } + + @Override + public Object[] getParameters() { + List<Object[]> list = new ArrayList<Object[]>(); + for(String parameterArray : combinedParameters.value()) { + list.add(Utils.splitAtCommaOrPipe(parameterArray)); + } + + return Cartesian.getCartesianProductOf(list); + } +} diff --git a/src/main/java/junitparams/internal/InvokeParameterisedMethod.java b/src/main/java/junitparams/internal/InvokeParameterisedMethod.java new file mode 100644 index 0000000..34024d5 --- /dev/null +++ b/src/main/java/junitparams/internal/InvokeParameterisedMethod.java @@ -0,0 +1,237 @@ +package junitparams.internal; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorManager; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.math.BigDecimal; + +import org.junit.runner.Description; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.Statement; + +import junitparams.converters.ConversionFailedException; +import junitparams.converters.ConvertParam; +import junitparams.converters.ParamAnnotation; +import junitparams.converters.ParamConverter; + +/** + * JUnit invoker for parameterised test methods + * + * @author Pawel Lipinski + */ +public class InvokeParameterisedMethod extends Statement { + + private final Object[] params; + private final FrameworkMethod testMethod; + private final Object testClass; + private final String uniqueMethodId; + + public InvokeParameterisedMethod(FrameworkMethod testMethod, Object testClass, Object params, int paramSetIdx) { + this.testMethod = testMethod; + this.testClass = testClass; + this.uniqueMethodId = Utils.uniqueMethodId(paramSetIdx - 1, params, testMethod.getName()); + try { + if (params instanceof String) + this.params = castParamsFromString((String) params); + else { + this.params = castParamsFromObjects(params); + } + } catch (ConversionFailedException e) { + throw new RuntimeException(e); + } + } + + private Object[] castParamsFromString(String params) throws ConversionFailedException { + Object[] columns = null; + try { + columns = Utils.splitAtCommaOrPipe(params); + columns = castParamsUsingConverters(columns); + } catch (RuntimeException e) { + new IllegalArgumentException("Cannot parse parameters. Did you use ',' or '|' as column separator? " + + params, e).printStackTrace(); + } + + return columns; + } + + private Object[] castParamsFromObjects(Object params) throws ConversionFailedException { + Object[] paramset = Utils.safelyCastParamsToArray(params); + + try { + return castParamsUsingConverters(paramset); + } catch (ConversionFailedException e) { + throw e; + } catch (Exception e) { + Class<?>[] typesOfParameters = createArrayOfTypesOf(paramset); + Object resultParam = createObjectOfExpectedTypeBasedOnParams(paramset, typesOfParameters); + return new Object[]{resultParam}; + } + } + + private Object createObjectOfExpectedTypeBasedOnParams(Object[] paramset, Class<?>[] typesOfParameters) { + Object resultParam; + + try { + if (testMethod.getMethod().getParameterTypes()[0].isArray()) { + resultParam = Array.newInstance(typesOfParameters[0], paramset.length); + for (int i = 0; i < paramset.length; i++) { + ((Object[]) resultParam)[i] = paramset[i]; + } + } else { + resultParam = testMethod.getMethod().getParameterTypes()[0].getConstructor(typesOfParameters).newInstance(paramset); + } + } catch (Exception e) { + throw new IllegalStateException("While trying to create object of class " + testMethod.getMethod().getParameterTypes()[0] + + " could not find constructor with arguments matching (type-wise) the ones given in parameters.", e); + } + return resultParam; + } + + private Class<?>[] createArrayOfTypesOf(Object[] paramset) { + Class<?>[] parametersBasedOnValues = new Class<?>[paramset.length]; + for (int i = 0; i < paramset.length; i++) { + parametersBasedOnValues[i] = paramset[i].getClass(); + } + return parametersBasedOnValues; + } + + private Object[] castParamsUsingConverters(Object[] columns) throws ConversionFailedException { + Class<?>[] expectedParameterTypes = testMethod.getMethod().getParameterTypes(); + + if (testMethodParamsHasVarargs(columns, expectedParameterTypes)) { + columns = columnsWithVarargs(columns, expectedParameterTypes); + } + + Annotation[][] parameterAnnotations = testMethod.getMethod().getParameterAnnotations(); + verifySameSizeOfArrays(columns, expectedParameterTypes); + columns = castAllParametersToProperTypes(columns, expectedParameterTypes, parameterAnnotations); + return columns; + } + + private Object[] columnsWithVarargs(Object[] columns, Class<?>[] expectedParameterTypes) { + Object[] allParameters = standardParameters(columns, expectedParameterTypes); + allParameters[allParameters.length - 1] = varargsParameters(columns, expectedParameterTypes); + return allParameters; + } + + private Object[] varargsParameters(Object[] columns, Class<?>[] expectedParameterTypes) { + Class<?> varArgType = expectedParameterTypes[expectedParameterTypes.length - 1].getComponentType(); + Object[] varArgsParameters = (Object[]) Array.newInstance(varArgType, columns.length - expectedParameterTypes.length + 1); + for (int i = 0; i < varArgsParameters.length; i++) { + varArgsParameters[i] = columns[i + expectedParameterTypes.length - 1]; + } + return varArgsParameters; + } + + private Object[] standardParameters(Object[] columns, Class<?>[] expectedParameterTypes) { + Object[] standardParameters = new Object[expectedParameterTypes.length]; + for (int i = 0; i < standardParameters.length - 1; i++) { + standardParameters[i] = columns[i]; + } + return standardParameters; + } + + private boolean testMethodParamsHasVarargs(Object[] columns, Class<?>[] expectedParameterTypes) { + int last = expectedParameterTypes.length - 1; + if (columns[last] == null) { + return false; + } + return expectedParameterTypes.length <= columns.length + && expectedParameterTypes[last].isArray() + && expectedParameterTypes[last].getComponentType().equals(columns[last].getClass()); + } + + private Object[] castAllParametersToProperTypes(Object[] columns, Class<?>[] expectedParameterTypes, + Annotation[][] parameterAnnotations) throws ConversionFailedException { + Object[] result = new Object[columns.length]; + + for (int i = 0; i < columns.length; i++) { + if (parameterAnnotations[i].length == 0) + result[i] = castParameterDirectly(columns[i], expectedParameterTypes[i]); + else + result[i] = castParameterUsingConverter(columns[i], parameterAnnotations[i]); + } + + return result; + } + + private Object castParameterUsingConverter(Object param, Annotation[] annotations) throws ConversionFailedException { + for (Annotation annotation : annotations) { + if (ParamAnnotation.matches(annotation)) { + return ParamAnnotation.convert(annotation, param); + } + if (annotation.annotationType().isAssignableFrom(ConvertParam.class)) { + Class<? extends ParamConverter<?>> converterClass = ((ConvertParam) annotation).value(); + String options = ((ConvertParam) annotation).options(); + try { + return converterClass.newInstance().convert(param, options); + } catch (ConversionFailedException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Your ParamConverter class must have a public no-arg constructor!", e); + } + } + } + return param; + } + + @SuppressWarnings("unchecked") + private Object castParameterDirectly(Object object, Class clazz) { + if (object == null || clazz.isInstance(object) || (!(object instanceof String) && clazz.isPrimitive())) + return object; + if (clazz.isEnum()) + return (Enum.valueOf(clazz, (String) object)); + if (clazz.isAssignableFrom(String.class)) + return object.toString(); + if (clazz.isAssignableFrom(Class.class)) + try { + return Class.forName((String) object); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Parameter class (" + object + ") not found", e); + } + if (clazz.isAssignableFrom(Integer.TYPE) || clazz.isAssignableFrom(Integer.class)) + return Integer.parseInt((String) object); + if (clazz.isAssignableFrom(Short.TYPE) || clazz.isAssignableFrom(Short.class)) + return Short.parseShort((String) object); + if (clazz.isAssignableFrom(Long.TYPE) || clazz.isAssignableFrom(Long.class)) + return Long.parseLong((String) object); + if (clazz.isAssignableFrom(Float.TYPE) || clazz.isAssignableFrom(Float.class)) + return Float.parseFloat((String) object); + if (clazz.isAssignableFrom(Double.TYPE) || clazz.isAssignableFrom(Double.class)) + return Double.parseDouble((String) object); + if (clazz.isAssignableFrom(Boolean.TYPE) || clazz.isAssignableFrom(Boolean.class)) + return Boolean.parseBoolean((String) object); + if (clazz.isAssignableFrom(Character.TYPE) || clazz.isAssignableFrom(Character.class)) + return object.toString().charAt(0); + if (clazz.isAssignableFrom(Byte.TYPE) || clazz.isAssignableFrom(Byte.class)) + return Byte.parseByte((String) object); + if (clazz.isAssignableFrom(BigDecimal.class)) + return new BigDecimal((String) object); + PropertyEditor editor = PropertyEditorManager.findEditor(clazz); + if (editor != null) { + editor.setAsText((String) object); + return editor.getValue(); + } + throw new IllegalArgumentException("Parameter type (" + clazz.getName() + ") cannot be handled!" + + " Only primitive types, BigDecimals and Strings can be used."); + } + + private void verifySameSizeOfArrays(Object[] columns, Class<?>[] parameterTypes) { + if (parameterTypes.length != columns.length) + throw new IllegalArgumentException( + "Number of parameters inside @Parameters annotation doesn't match the number of test method parameters.\nThere are " + + columns.length + " parameters in annotation, while there's " + parameterTypes.length + " parameters in the " + + testMethod.getName() + " method."); + } + + boolean matchesDescription(Description description) { + return description.hashCode() == uniqueMethodId.hashCode(); + } + + @Override + public void evaluate() throws Throwable { + testMethod.invokeExplosively(testClass, params == null ? new Object[]{params} : params); + } + +} diff --git a/src/main/java/junitparams/internal/ParameterisedTestClassRunner.java b/src/main/java/junitparams/internal/ParameterisedTestClassRunner.java new file mode 100644 index 0000000..23daf88 --- /dev/null +++ b/src/main/java/junitparams/internal/ParameterisedTestClassRunner.java @@ -0,0 +1,177 @@ +package junitparams.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; + +/** + * Testclass-level functionalities to handle parameters from a JUnit runner + * class. + * + * @author Pawel Lipinski + */ +public class ParameterisedTestClassRunner { + + protected Map<TestMethod, ParameterisedTestMethodRunner> parameterisedMethods = new HashMap<TestMethod, ParameterisedTestMethodRunner>(); + protected Map<FrameworkMethod, TestMethod> testMethods = new HashMap<FrameworkMethod, TestMethod>(); + protected List<TestMethod> testMethodsList; + + /** + * Creates a runner for a given test class. Computes all the test methods + * that are annotated as tests. Retrieves and caches all parameter values. + * + * @param testClass + */ + public ParameterisedTestClassRunner(TestClass testClass) { + computeTestMethods(testClass); + fillTestMethodsMap(); + computeFrameworkMethods(); + } + + protected void computeTestMethods(TestClass testClass) { + testMethodsList = TestMethod.listFrom(testClass.getAnnotatedMethods(Test.class), testClass); + } + + private void fillTestMethodsMap() { + for (TestMethod testMethod : testMethodsList) + testMethods.put(testMethod.frameworkMethod(), testMethod); + } + + /** + * Returns a list of <code>FrameworkMethod</code>s. Handles both + * parameterised methods (counts them as many times as many paramsets they + * have) and nonparameterised methods (just counts them once). + * + * @return a list of FrameworkMethod objects + */ + public List<FrameworkMethod> computeFrameworkMethods() { + List<FrameworkMethod> resultMethods = new ArrayList<FrameworkMethod>(); + + for (TestMethod testMethod : testMethodsList) { + if (testMethod.isParameterised()) + addTestMethodForEachParamSet(resultMethods, testMethod); + else + addTestMethodOnce(resultMethods, testMethod); + } + + return resultMethods; + } + + /** + * Returns a list of <code>FrameworkMethod</code>s - once per method, like + * there were no parameters. + * For JUnit to build names for IDE. + */ + public List<FrameworkMethod> returnListOfMethods() { + List<FrameworkMethod> resultMethods = new ArrayList<FrameworkMethod>(); + + for (TestMethod testMethod : testMethodsList) { + addTestMethodOnce(resultMethods, testMethod); + cacheMethodRunner(testMethod); + testMethod.warnIfNoParamsGiven(); + } + + return resultMethods; + } + + private void addTestMethodForEachParamSet(List<FrameworkMethod> resultMethods, TestMethod testMethod) { + if (testMethod.isNotIgnored()) { + int paramSetSize = testMethod.parametersSets().length; + for (int i = 0; i < paramSetSize; i++) + addTestMethodOnce(resultMethods, testMethod); + } else { + addTestMethodOnce(resultMethods, testMethod); + } + } + + private void addTestMethodOnce(List<FrameworkMethod> resultMethods, TestMethod testMethod) { + resultMethods.add(testMethod.frameworkMethod()); + } + + private void cacheMethodRunner(TestMethod testMethod) { + if (!parameterisedMethods.containsKey(testMethod)) + parameterisedMethods.put(testMethod, new ParameterisedTestMethodRunner(testMethod)); + } + + /** + * Returns a InvokeParameterisedMethod for parameterised methods and null + * for nonparameterised + * + * @param method Test method + * @param testClass + * @return a Statement with the invoker for the parameterised method + */ + public Statement parameterisedMethodInvoker(FrameworkMethod method, Object testClass) { + TestMethod testMethod = testMethods.get(method); + + if (!testMethod.isParameterised()) + return null; + + return buildMethodInvoker(method, testClass, testMethod); + } + + private Statement buildMethodInvoker(FrameworkMethod method, Object testClass, TestMethod testMethod) { + ParameterisedTestMethodRunner parameterisedMethod = parameterisedMethods.get(testMethod); + + return new InvokeParameterisedMethod( + method, testClass, parameterisedMethod.currentParamsFromAnnotation(), parameterisedMethod.count()); + } + + /** + * Tells if method should be run by this runner. + * + * @param testMethod + * @return true, iff testMethod should be run by this runner. + */ + public boolean shouldRun(TestMethod testMethod) { + return testMethod.isParameterised(); + } + + /** + * Executes parameterised method. + * + * @param method + * @param methodInvoker + * @param notifier + */ + public void runParameterisedTest(TestMethod method, Statement methodInvoker, RunNotifier notifier) { + parameterisedMethods.get(method).runTestMethod(methodInvoker, notifier); + } + + /** + * Returns description of a parameterised method. + * + * @param method TODO + * @return Description of a method or null if it's not parameterised. + */ + public Description describeParameterisedMethod(FrameworkMethod method) { + TestMethod testMethod = testMethods.get(method); + + if (!testMethod.isParameterised()) + return null; + + return testMethod.describe(); + } + + /** + * Returns a cached TestMethod object related to the given FrameworkMethod. + * This object has all the params already retrieved, so use this one and not + * TestMethod's constructor if you want to have everything retrieved once + * and cached. + * + * @param method + * @return a cached TestMethod instance + */ + public TestMethod testMethodFor(FrameworkMethod method) { + return testMethods.get(method); + } + +} diff --git a/src/main/java/junitparams/internal/ParameterisedTestMethodRunner.java b/src/main/java/junitparams/internal/ParameterisedTestMethodRunner.java new file mode 100644 index 0000000..9573048 --- /dev/null +++ b/src/main/java/junitparams/internal/ParameterisedTestMethodRunner.java @@ -0,0 +1,108 @@ +package junitparams.internal; + +import java.lang.reflect.Field; + +import org.junit.internal.AssumptionViolatedException; +import org.junit.internal.runners.model.EachTestNotifier; +import org.junit.runner.Description; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.model.Statement; + +/** + * Testmethod-level functionalities for parameterised tests + * + * @author Pawel Lipinski + */ +public class ParameterisedTestMethodRunner { + + public final TestMethod method; + private int count; + + public ParameterisedTestMethodRunner(TestMethod testMethod) { + this.method = testMethod; + } + + public int nextCount() { + return count++; + } + + public int count() { + return count; + } + + Object currentParamsFromAnnotation() { + return method.parametersSets()[nextCount()]; + } + + void runTestMethod(Statement methodInvoker, RunNotifier notifier) { + Description methodWithParams = findChildForParams(methodInvoker, method.describe()); + + runMethodInvoker(notifier, methodInvoker, methodWithParams); + } + + private void runMethodInvoker(RunNotifier notifier, Statement methodInvoker, Description methodWithParams) { + EachTestNotifier eachNotifier = new EachTestNotifier(notifier, methodWithParams); + eachNotifier.fireTestStarted(); + try { + methodInvoker.evaluate(); + } catch (AssumptionViolatedException e) { + eachNotifier.addFailedAssumption(e); + } catch (Throwable e) { + eachNotifier.addFailure(e); + } finally { + eachNotifier.fireTestFinished(); + } + } + + private Description findChildForParams(Statement methodInvoker, Description methodDescription) { + if (System.getProperty("JUnitParams.flat") != null) + return methodDescription; + + InvokeParameterisedMethod parameterisedInvoker = findParameterisedMethodInvokerInChain(methodInvoker); + + for (Description child : methodDescription.getChildren()) { + if (parameterisedInvoker.matchesDescription(child)) + return child; + } + return null; + } + + private InvokeParameterisedMethod findParameterisedMethodInvokerInChain(Statement methodInvoker) { + while (methodInvoker != null && !(methodInvoker instanceof InvokeParameterisedMethod)) + methodInvoker = nextChainedInvoker(methodInvoker); + + if (methodInvoker == null) + throw new RuntimeException("Cannot find invoker for the parameterised method. Using wrong JUnit version?"); + + return (InvokeParameterisedMethod) methodInvoker; + } + + private Statement nextChainedInvoker(Statement methodInvoker) { + Field[] declaredFields = methodInvoker.getClass().getDeclaredFields(); + + for (Field field : declaredFields) { + Statement statement = statementOrNull(methodInvoker, field); + if (statement != null) + return statement; + } + + return null; + } + + private Statement statementOrNull(Statement methodInvoker, Field field) { + if (Statement.class.isAssignableFrom(field.getType())) + return getOriginalStatement(methodInvoker, field); + + return null; + } + + private Statement getOriginalStatement(Statement methodInvoker, Field field) { + field.setAccessible(true); + try { + return (Statement) field.get(methodInvoker); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/junitparams/internal/ParametrizedTestMethodsFilter.java b/src/main/java/junitparams/internal/ParametrizedTestMethodsFilter.java new file mode 100644 index 0000000..905934c --- /dev/null +++ b/src/main/java/junitparams/internal/ParametrizedTestMethodsFilter.java @@ -0,0 +1,37 @@ +package junitparams.internal; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.runner.manipulation.Filter; +import org.junit.runners.model.FrameworkMethod; + +import junitparams.JUnitParamsRunner; + +public class ParametrizedTestMethodsFilter { + private final junitparams.JUnitParamsRunner jUnitParamsRunner; + + private final Filter filter; + + public ParametrizedTestMethodsFilter(junitparams.JUnitParamsRunner jUnitParamsRunner, Filter filter) { + this.jUnitParamsRunner = jUnitParamsRunner; + this.filter = filter; + } + + public ParametrizedTestMethodsFilter(JUnitParamsRunner jUnitParamsRunner) { + this.jUnitParamsRunner = jUnitParamsRunner; + this.filter = Filter.ALL; + } + + public List<FrameworkMethod> filteredMethods(List<FrameworkMethod> frameworkMethods) { + List<FrameworkMethod> filteredMethods = new ArrayList<FrameworkMethod>(); + + for (FrameworkMethod frameworkMethod : frameworkMethods) { + if (filter.shouldRun(jUnitParamsRunner.describeMethod(frameworkMethod))) { + filteredMethods.add(frameworkMethod); + } + } + + return filteredMethods; + } +}
\ No newline at end of file diff --git a/src/main/java/junitparams/internal/TestMethod.java b/src/main/java/junitparams/internal/TestMethod.java new file mode 100644 index 0000000..6125803 --- /dev/null +++ b/src/main/java/junitparams/internal/TestMethod.java @@ -0,0 +1,141 @@ +package junitparams.internal; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.Ignore; +import org.junit.runner.Description; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.TestClass; + +import junitparams.internal.annotation.FrameworkMethodAnnotations; +import junitparams.internal.parameters.ParametersReader; +import junitparams.naming.MacroSubstitutionNamingStrategy; +import junitparams.naming.TestCaseNamingStrategy; + +/** + * A wrapper for a test method + * + * @author Pawel Lipinski + */ +public class TestMethod { + private FrameworkMethod frameworkMethod; + FrameworkMethodAnnotations frameworkMethodAnnotations; + private Class<?> testClass; + private ParametersReader parametersReader; + private Object[] cachedParameters; + private TestCaseNamingStrategy namingStrategy; + + public TestMethod(FrameworkMethod method, TestClass testClass) { + this.frameworkMethod = method; + this.testClass = testClass.getJavaClass(); + frameworkMethodAnnotations = new FrameworkMethodAnnotations(method); + parametersReader = new ParametersReader(testClass(), frameworkMethod); + + namingStrategy = new MacroSubstitutionNamingStrategy(this); + } + + public String name() { + return frameworkMethod.getName(); + } + + public static List<TestMethod> listFrom(List<FrameworkMethod> annotatedMethods, TestClass testClass) { + List<TestMethod> methods = new ArrayList<TestMethod>(); + + for (FrameworkMethod frameworkMethod : annotatedMethods) + methods.add(new TestMethod(frameworkMethod, testClass)); + + return methods; + } + + @Override + public int hashCode() { + return frameworkMethod.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof TestMethod) + && hasTheSameNameAsFrameworkMethod((TestMethod) obj) + && hasTheSameParameterTypesAsFrameworkMethod((TestMethod) obj); + } + + private boolean hasTheSameNameAsFrameworkMethod(TestMethod testMethod) { + return frameworkMethod.getName().equals(testMethod.frameworkMethod.getName()); + } + + private boolean hasTheSameParameterTypesAsFrameworkMethod(TestMethod testMethod) { + Class<?>[] frameworkMethodParameterTypes = frameworkMethod.getMethod().getParameterTypes(); + Class<?>[] testMethodParameterTypes = testMethod.frameworkMethod.getMethod().getParameterTypes(); + return Arrays.equals(frameworkMethodParameterTypes, testMethodParameterTypes); + } + + Class<?> testClass() { + return testClass; + } + + public boolean isIgnored() { + return hasIgnoredAnnotation() || hasNoParameters(); + } + + private boolean hasIgnoredAnnotation() { + return frameworkMethodAnnotations.hasAnnotation(Ignore.class); + } + + private boolean hasNoParameters() { + return isParameterised() && parametersSets().length == 0; + } + + public boolean isNotIgnored() { + return !isIgnored(); + } + + public <T extends Annotation> T getAnnotation(Class<T> annotationType) { + return frameworkMethodAnnotations.getAnnotation(annotationType); + } + + Description describe() { + if (isNotIgnored() && !describeFlat()) { + Description parametrised = Description.createSuiteDescription(name()); + Object[] params = parametersSets(); + for (int i = 0; i < params.length; i++) { + Object paramSet = params[i]; + String name = namingStrategy.getTestCaseName(i, paramSet); + String uniqueMethodId = Utils.uniqueMethodId(i, paramSet, name()); + + parametrised.addChild( + Description.createTestDescription(testClass().getName(), name, uniqueMethodId) + ); + } + return parametrised; + } else { + return Description.createTestDescription(testClass(), name(), frameworkMethodAnnotations.allAnnotations()); + } + } + + private boolean describeFlat() { + return System.getProperty("JUnitParams.flat") != null; + } + + public Object[] parametersSets() { + if (cachedParameters == null) { + cachedParameters = parametersReader.read(); + } + return cachedParameters; + } + + void warnIfNoParamsGiven() { + if (isNotIgnored() && isParameterised() && parametersSets().length == 0) + System.err.println("Method " + name() + " gets empty list of parameters, so it's being ignored!"); + } + + public FrameworkMethod frameworkMethod() { + return frameworkMethod; + } + + boolean isParameterised() { + return frameworkMethodAnnotations.isParametrised(); + } +} diff --git a/src/main/java/junitparams/internal/Utils.java b/src/main/java/junitparams/internal/Utils.java new file mode 100644 index 0000000..0e10adf --- /dev/null +++ b/src/main/java/junitparams/internal/Utils.java @@ -0,0 +1,164 @@ +package junitparams.internal; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Some String utils to handle parameterised tests' results. + * + * @author Pawel Lipinski + */ +public class Utils { + public static final String REGEX_ALL_NEWLINES = "(\\r\\n|\\n|\\r)"; + + public static String stringify(Object paramSet, int paramIdx) { + String result = "[" + paramIdx + "] "; + + return result + stringify(paramSet); + } + + public static String stringify(Object paramSet) { + String result; + if (paramSet == null) + result = "null"; + else if (paramSet instanceof String) + result = paramSet.toString(); + else + result = asCsvString(safelyCastParamsToArray(paramSet)); + + return trimSpecialChars(result); + } + + public static String getParameterStringByIndexOrEmpty(Object paramSet, int parameterIndex) { + Object[] params = safelyCastParamsToArray(paramSet); + if (paramSet instanceof String) { + params = splitAtCommaOrPipe((String) paramSet); + } + if (parameterIndex >= 0 && parameterIndex < params.length) { + return addParamToResult("", params[parameterIndex]); + } + + return ""; + } + + public static String[] splitAtCommaOrPipe(String input) { + ArrayList<String> result = new ArrayList<String>(); + + char character = '\0'; + char previousCharacter; + + StringBuilder value = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + previousCharacter = character; + character = input.charAt(i); + + if (character == ',' || character == '|') { + if (previousCharacter == '\\') { + value.setCharAt(value.length() - 1, character); + continue; + } + result.add(value.toString().trim()); + value = new StringBuilder(); + continue; + } + + value.append(character); + } + result.add(value.toString().trim()); + + return result.toArray(new String[]{}); + } + + private static String trimSpecialChars(String result) { + return result.replace('(', '[').replace(')', ']').replaceAll(REGEX_ALL_NEWLINES, " "); + } + + static Object[] safelyCastParamsToArray(Object paramSet) { + final Object[] params; + if (paramSet instanceof Object[]) { + params = (Object[]) paramSet; + } else { + params = new Object[]{paramSet}; + } + return params; + } + + private static String asCsvString(Object[] params) { + if (params == null) + return "null"; + + if (params.length == 0) + return ""; + + String result = ""; + + for (int i = 0; i < params.length - 1; i++) { + Object param = params[i]; + result = addParamToResult(result, param) + ", "; + } + result = addParamToResult(result, params[params.length - 1]); + + return result; + } + + private static String addParamToResult(String result, Object param) { + if (param == null) + result += "null"; + else if (param.getClass().isArray()) + result += convertAnyArrayToString(param); + else if (hasOverridenToStringMethod(param)) + result += param.toString(); + else + result += param.getClass().getSimpleName(); + + return result; + } + + private static boolean hasOverridenToStringMethod(Object param) { + Method[] methods = param.getClass().getMethods(); + for (Method method : methods) { + if (method.getName().equals("toString") && overridesMethod(method)) { + return true; + } + } + return false; + } + + private static boolean overridesMethod(Method method) { + return !method.getDeclaringClass().equals(Object.class); + } + + static String uniqueMethodId(int index, Object paramSet, String methodName) { + return stringify(paramSet, index) + " (" + methodName + ")"; + } + + private static String convertAnyArrayToString(Object arrayAsObject) { + if (arrayAsObject.getClass().getComponentType().isPrimitive()) { + return convertFromArrayOfPrimitives(arrayAsObject); + } else { + return Arrays.toString((Object[]) arrayAsObject); + } + } + + private static final String convertFromArrayOfPrimitives(Object arrayOfPrimitives) { + String componentType = arrayOfPrimitives.getClass().getComponentType().getName(); + if ("byte".equals(componentType)) { + return Arrays.toString((byte[]) arrayOfPrimitives); + } else if ("short".equals(componentType)) { + return Arrays.toString((short[]) arrayOfPrimitives); + } else if ("int".equals(componentType)) { + return Arrays.toString((int[]) arrayOfPrimitives); + } else if ("long".equals(componentType)) { + return Arrays.toString((long[]) arrayOfPrimitives); + } else if ("float".equals(componentType)) { + return Arrays.toString((float[]) arrayOfPrimitives); + } else if ("double".equals(componentType)) { + return Arrays.toString((double[]) arrayOfPrimitives); + } else if ("boolean".equals(componentType)) { + return Arrays.toString((boolean[]) arrayOfPrimitives); + } else { + return Arrays.toString((char[]) arrayOfPrimitives); + } + } +} diff --git a/src/main/java/junitparams/internal/annotation/CustomParametersDescriptor.java b/src/main/java/junitparams/internal/annotation/CustomParametersDescriptor.java new file mode 100644 index 0000000..7f0729e --- /dev/null +++ b/src/main/java/junitparams/internal/annotation/CustomParametersDescriptor.java @@ -0,0 +1,30 @@ +package junitparams.internal.annotation; + +import java.lang.annotation.Annotation; + +import junitparams.custom.CustomParameters; +import junitparams.custom.ParametersProvider; + +public class CustomParametersDescriptor { + + private final Annotation customAnnotation; + + private final Class<? extends ParametersProvider> provider; + + public CustomParametersDescriptor(CustomParameters customParameters) { + this(customParameters, customParameters); + } + + public CustomParametersDescriptor(CustomParameters customParameters, Annotation customAnnotation) { + this.provider = customParameters.provider(); + this.customAnnotation = customAnnotation; + } + + public Class<? extends ParametersProvider> provider() { + return provider; + } + + public Annotation annotation() { + return customAnnotation; + } +} diff --git a/src/main/java/junitparams/internal/annotation/FrameworkMethodAnnotations.java b/src/main/java/junitparams/internal/annotation/FrameworkMethodAnnotations.java new file mode 100644 index 0000000..326bb21 --- /dev/null +++ b/src/main/java/junitparams/internal/annotation/FrameworkMethodAnnotations.java @@ -0,0 +1,53 @@ +package junitparams.internal.annotation; + +import java.lang.annotation.Annotation; + +import org.junit.runners.model.FrameworkMethod; + +import junitparams.Parameters; +import junitparams.custom.CustomParameters; + +public class FrameworkMethodAnnotations { + + private final FrameworkMethod frameworkMethod; + + public FrameworkMethodAnnotations(FrameworkMethod frameworkMethod) { + this.frameworkMethod = frameworkMethod; + } + + public boolean isParametrised() { + return hasAnnotation(Parameters.class) + || hasCustomParameters(); + } + + public Annotation[] allAnnotations() { + return frameworkMethod.getAnnotations(); + } + + public <T extends Annotation> T getAnnotation(Class<T> annotationType) { + return frameworkMethod.getAnnotation(annotationType); + } + + public boolean hasAnnotation(Class<? extends Annotation> annotation) { + return getAnnotation(annotation) != null; + } + + public boolean hasCustomParameters() { + return getCustomParameters() != null; + } + + public CustomParametersDescriptor getCustomParameters() { + CustomParameters customParameters = frameworkMethod.getAnnotation(CustomParameters.class); + if (customParameters != null) { + return new CustomParametersDescriptor(customParameters); + } + + for (Annotation annotation : frameworkMethod.getAnnotations()) { + customParameters = annotation.annotationType().getAnnotation(CustomParameters.class); + if (customParameters != null) { + return new CustomParametersDescriptor(customParameters, annotation); + } + } + return null; + } +} diff --git a/src/main/java/junitparams/internal/parameters/ParametersFromCustomProvider.java b/src/main/java/junitparams/internal/parameters/ParametersFromCustomProvider.java new file mode 100644 index 0000000..09dbf99 --- /dev/null +++ b/src/main/java/junitparams/internal/parameters/ParametersFromCustomProvider.java @@ -0,0 +1,38 @@ +package junitparams.internal.parameters; + +import org.junit.runners.model.FrameworkMethod; + +import junitparams.custom.ParametersProvider; +import junitparams.internal.annotation.CustomParametersDescriptor; +import junitparams.internal.annotation.FrameworkMethodAnnotations; + +public class ParametersFromCustomProvider implements ParametrizationStrategy { + + private final FrameworkMethodAnnotations frameworkMethodAnnotations; + + public ParametersFromCustomProvider(FrameworkMethod frameworkMethod) { + frameworkMethodAnnotations = new FrameworkMethodAnnotations(frameworkMethod); + } + + @Override + public boolean isApplicable() { + return frameworkMethodAnnotations.hasCustomParameters(); + } + + @Override + public Object[] getParameters() { + CustomParametersDescriptor parameters = frameworkMethodAnnotations.getCustomParameters(); + ParametersProvider provider = instantiate(parameters.provider()); + provider.initialize(parameters.annotation()); + return provider.getParameters(); + } + + private ParametersProvider instantiate(Class<? extends ParametersProvider> providerClass) { + try { + return providerClass.newInstance(); + } catch (Exception e) { + throw new RuntimeException("Your Provider class must have a public no-arg constructor!", e); + } + } + +} diff --git a/src/main/java/junitparams/internal/parameters/ParametersFromExternalClassMethod.java b/src/main/java/junitparams/internal/parameters/ParametersFromExternalClassMethod.java new file mode 100644 index 0000000..051fde0 --- /dev/null +++ b/src/main/java/junitparams/internal/parameters/ParametersFromExternalClassMethod.java @@ -0,0 +1,29 @@ +package junitparams.internal.parameters; + +import junitparams.Parameters; +import org.junit.runners.model.FrameworkMethod; + +import javax.lang.model.type.NullType; + +class ParametersFromExternalClassMethod implements ParametrizationStrategy { + private ParamsFromMethodCommon paramsFromMethodCommon; + private Parameters annotation; + + ParametersFromExternalClassMethod(FrameworkMethod frameworkMethod) { + this.paramsFromMethodCommon = new ParamsFromMethodCommon(frameworkMethod); + annotation = frameworkMethod.getAnnotation(Parameters.class); + } + + @Override + public Object[] getParameters() { + Class<?> sourceClass = annotation.source(); + return paramsFromMethodCommon.paramsFromMethod(sourceClass); + } + + @Override + public boolean isApplicable() { + return annotation != null + && !annotation.source().isAssignableFrom(NullType.class) + && !annotation.method().isEmpty(); + } +}
\ No newline at end of file diff --git a/src/main/java/junitparams/internal/parameters/ParametersFromExternalClassProvideMethod.java b/src/main/java/junitparams/internal/parameters/ParametersFromExternalClassProvideMethod.java new file mode 100644 index 0000000..7bded6d --- /dev/null +++ b/src/main/java/junitparams/internal/parameters/ParametersFromExternalClassProvideMethod.java @@ -0,0 +1,80 @@ +package junitparams.internal.parameters; + +import junitparams.Parameters; +import org.junit.runners.model.FrameworkMethod; + +import javax.lang.model.type.NullType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class ParametersFromExternalClassProvideMethod implements ParametrizationStrategy { + private final ParamsFromMethodCommon paramsFromMethodCommon; + private Parameters annotation; + + ParametersFromExternalClassProvideMethod(FrameworkMethod frameworkMethod) { + this.paramsFromMethodCommon = new ParamsFromMethodCommon(frameworkMethod); + annotation = frameworkMethod.getAnnotation(Parameters.class); + } + + @Override + public Object[] getParameters() { + Class<?> sourceClass = annotation.source(); + return fillResultWithAllParamProviderMethods(sourceClass); + } + + @Override + public boolean isApplicable() { + return annotation != null + && !annotation.source().isAssignableFrom(NullType.class) + && annotation.method().isEmpty(); + } + + private Object[] fillResultWithAllParamProviderMethods(Class<?> sourceClass) { + if (sourceClass.isEnum()) { + return sourceClass.getEnumConstants(); + } + + List<Object> result = getParamsFromSourceHierarchy(sourceClass); + if (result.isEmpty()) + throw new RuntimeException( + "No methods starting with provide or they return no result in the parameters source class: " + + sourceClass.getName()); + + return result.toArray(); + } + + private List<Object> getParamsFromSourceHierarchy(Class<?> sourceClass) { + List<Object> result = new ArrayList<Object>(); + while (sourceClass.getSuperclass() != null) { + result.addAll(gatherParamsFromAllMethodsFrom(sourceClass)); + sourceClass = sourceClass.getSuperclass(); + } + + return result; + } + + private List<Object> gatherParamsFromAllMethodsFrom(Class<?> sourceClass) { + List<Object> result = new ArrayList<Object>(); + Method[] methods = sourceClass.getDeclaredMethods(); + for (Method prividerMethod : methods) { + if (prividerMethod.getName().startsWith("provide")) { + if (!Modifier.isStatic(prividerMethod.getModifiers())) { + throw new RuntimeException("Parameters source method " + + prividerMethod.getName() + + " is not declared as static. Change it to a static method."); + } + try { + result.addAll( + Arrays.asList(paramsFromMethodCommon.getDataFromMethod(prividerMethod))); + } catch (Exception e) { + throw new RuntimeException("Cannot invoke parameters source method: " + prividerMethod.getName(), + e); + } + } + } + return result; + } +}
\ No newline at end of file diff --git a/src/main/java/junitparams/internal/parameters/ParametersFromTestClassMethod.java b/src/main/java/junitparams/internal/parameters/ParametersFromTestClassMethod.java new file mode 100644 index 0000000..9d1ab7a --- /dev/null +++ b/src/main/java/junitparams/internal/parameters/ParametersFromTestClassMethod.java @@ -0,0 +1,31 @@ +package junitparams.internal.parameters; + +import javax.lang.model.type.NullType; + +import org.junit.runners.model.FrameworkMethod; + +import junitparams.Parameters; + +class ParametersFromTestClassMethod implements ParametrizationStrategy { + private ParamsFromMethodCommon paramsFromMethodCommon; + private Class<?> testClass; + private Parameters annotation; + + ParametersFromTestClassMethod(FrameworkMethod frameworkMethod, Class<?> testClass) { + paramsFromMethodCommon = new ParamsFromMethodCommon(frameworkMethod); + this.testClass = testClass; + annotation = frameworkMethod.getAnnotation(Parameters.class); + } + + @Override + public Object[] getParameters() { + return paramsFromMethodCommon.paramsFromMethod(testClass); + } + + @Override + public boolean isApplicable() { + return annotation != null + && annotation.source().isAssignableFrom(NullType.class) + && (!annotation.method().isEmpty() || paramsFromMethodCommon.containsDefaultParametersProvidingMethod(testClass)); + } +} diff --git a/src/main/java/junitparams/internal/parameters/ParametersFromValue.java b/src/main/java/junitparams/internal/parameters/ParametersFromValue.java new file mode 100644 index 0000000..943794d --- /dev/null +++ b/src/main/java/junitparams/internal/parameters/ParametersFromValue.java @@ -0,0 +1,23 @@ +package junitparams.internal.parameters; + +import junitparams.Parameters; +import org.junit.runners.model.FrameworkMethod; + +class ParametersFromValue implements ParametrizationStrategy { + + private final Parameters parametersAnnotation; + + ParametersFromValue(FrameworkMethod frameworkMethod) { + parametersAnnotation = frameworkMethod.getAnnotation(Parameters.class); + } + + @Override + public Object[] getParameters() { + return parametersAnnotation.value(); + } + + @Override + public boolean isApplicable() { + return parametersAnnotation != null && parametersAnnotation.value().length > 0; + } +}
\ No newline at end of file diff --git a/src/main/java/junitparams/internal/parameters/ParametersReader.java b/src/main/java/junitparams/internal/parameters/ParametersReader.java new file mode 100644 index 0000000..b0f5ec3 --- /dev/null +++ b/src/main/java/junitparams/internal/parameters/ParametersReader.java @@ -0,0 +1,55 @@ +package junitparams.internal.parameters; + +import java.util.List; + +import org.junit.runners.model.FrameworkMethod; + +import junitparams.FileParameters; +import junitparams.Parameters; + +import static java.lang.String.*; +import static java.util.Arrays.*; + +public class ParametersReader { + + public static final String ILLEGAL_STATE_EXCEPTION_MESSAGE + = format("Illegal usage of JUnitParams in method %s. " + + "Check that you have only used one supported parameters evaluation strategy. " + + "Common case is to use both %s and %s annotations.", + "%s", Parameters.class, FileParameters.class); + + private final FrameworkMethod frameworkMethod; + private final List<ParametrizationStrategy> strategies; + + public ParametersReader(Class<?> testClass, FrameworkMethod frameworkMethod) { + this.frameworkMethod = frameworkMethod; + + strategies = asList( + new ParametersFromCustomProvider(frameworkMethod), + new ParametersFromValue(frameworkMethod), + new ParametersFromExternalClassProvideMethod(frameworkMethod), + new ParametersFromExternalClassMethod(frameworkMethod), + new ParametersFromTestClassMethod(frameworkMethod, testClass) + ); + } + + public Object[] read() { + boolean strategyAlreadyFound = false; + Object[] parameters = new Object[]{}; + + for (ParametrizationStrategy strategy : strategies) { + if (strategy.isApplicable()) { + if (strategyAlreadyFound) { + illegalState(); + } + parameters = strategy.getParameters(); + strategyAlreadyFound = true; + } + } + return parameters; + } + + private void illegalState() { + throw new IllegalStateException(format(ILLEGAL_STATE_EXCEPTION_MESSAGE, frameworkMethod.getName())); + } +} diff --git a/src/main/java/junitparams/internal/parameters/ParametrizationStrategy.java b/src/main/java/junitparams/internal/parameters/ParametrizationStrategy.java new file mode 100644 index 0000000..b140edd --- /dev/null +++ b/src/main/java/junitparams/internal/parameters/ParametrizationStrategy.java @@ -0,0 +1,6 @@ +package junitparams.internal.parameters; + +interface ParametrizationStrategy { + Object[] getParameters(); + boolean isApplicable(); +} diff --git a/src/main/java/junitparams/internal/parameters/ParamsFromMethodCommon.java b/src/main/java/junitparams/internal/parameters/ParamsFromMethodCommon.java new file mode 100644 index 0000000..d2ec124 --- /dev/null +++ b/src/main/java/junitparams/internal/parameters/ParamsFromMethodCommon.java @@ -0,0 +1,150 @@ +package junitparams.internal.parameters; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.junit.runners.model.FrameworkMethod; + +import junitparams.Parameters; + +class ParamsFromMethodCommon { + private FrameworkMethod frameworkMethod; + + ParamsFromMethodCommon(FrameworkMethod frameworkMethod) { + this.frameworkMethod = frameworkMethod; + } + + Object[] paramsFromMethod(Class<?> sourceClass) { + String methodAnnotation = frameworkMethod.getAnnotation(Parameters.class).method(); + + if (methodAnnotation.isEmpty()) { + return invokeMethodWithParams(defaultMethodName(), sourceClass); + } + + List<Object> result = new ArrayList<Object>(); + for (String methodName : methodAnnotation.split(",")) { + for (Object param : invokeMethodWithParams(methodName.trim(), sourceClass)) + result.add(param); + } + + return result.toArray(); + } + + Object[] getDataFromMethod(Method providerMethod) throws IllegalAccessException, InvocationTargetException { + return encapsulateParamsIntoArrayIfSingleParamsetPassed((Object[]) providerMethod.invoke(null)); + } + + boolean containsDefaultParametersProvidingMethod(Class<?> sourceClass) { + return findMethodInTestClassHierarchy(defaultMethodName(), sourceClass) != null; + } + + private String defaultMethodName() { + return "parametersFor" + frameworkMethod.getName().substring(0, 1).toUpperCase() + + this.frameworkMethod.getName().substring(1); + } + + private Object[] invokeMethodWithParams(String methodName, Class<?> sourceClass) { + Method providerMethod = findMethodInTestClassHierarchy(methodName, sourceClass); + if (providerMethod == null) { + throw new RuntimeException("Could not find method: " + methodName + " so no params were used."); + } + + return invokeParamsProvidingMethod(providerMethod, sourceClass); + } + + @SuppressWarnings("unchecked") + private Object[] invokeParamsProvidingMethod(Method provideMethod, Class<?> sourceClass) { + try { + Object testObject = sourceClass.newInstance(); + provideMethod.setAccessible(true); + Object result = provideMethod.invoke(testObject); + + if (Object[].class.isAssignableFrom(result.getClass())) { + Object[] params = (Object[]) result; + return encapsulateParamsIntoArrayIfSingleParamsetPassed(params); + } + + if (Iterable.class.isAssignableFrom(result.getClass())) { + try { + ArrayList<Object[]> res = new ArrayList<Object[]>(); + for (Object[] paramSet : (Iterable<Object[]>) result) + res.add(paramSet); + return res.toArray(); + } catch (ClassCastException e1) { + // Iterable with consecutive paramsets, each of one param + ArrayList<Object> res = new ArrayList<Object>(); + for (Object param : (Iterable<?>) result) + res.add(new Object[]{param}); + return res.toArray(); + } + } + + if (Iterator.class.isAssignableFrom(result.getClass())) { + Object iteratedElement = null; + try { + ArrayList<Object[]> res = new ArrayList<Object[]>(); + Iterator<Object[]> iterator = (Iterator<Object[]>) result; + while (iterator.hasNext()) { + iteratedElement = iterator.next(); + // ClassCastException will occur in the following line + // if the iterator is actually Iterator<Object> in Java 7 + res.add((Object[]) iteratedElement); + } + return res.toArray(); + } catch (ClassCastException e1) { + // Iterator with consecutive paramsets, each of one param + ArrayList<Object> res = new ArrayList<Object>(); + Iterator<?> iterator = (Iterator<?>) result; + // The first element is already stored in iteratedElement + res.add(iteratedElement); + while (iterator.hasNext()) { + res.add(new Object[]{iterator.next()}); + } + return res.toArray(); + } + } + + throw new ClassCastException(); + + } catch (ClassCastException e) { + throw new RuntimeException("The return type of: " + provideMethod.getName() + " defined in class " + + sourceClass + " is not Object[][] nor Iterable<Object[]>. Fix it!", e); + } catch (Exception e) { + throw new RuntimeException("Could not invoke method: " + provideMethod.getName() + " defined in class " + + sourceClass + " so no params were used.", e); + } + } + + private Method findMethodInTestClassHierarchy(String methodName, Class<?> sourceClass) { + Class<?> declaringClass = sourceClass; + while (declaringClass.getSuperclass() != null) { + try { + return declaringClass.getDeclaredMethod(methodName); + } catch (Exception ignore) { + } + declaringClass = declaringClass.getSuperclass(); + } + return null; + } + + private Object[] encapsulateParamsIntoArrayIfSingleParamsetPassed(Object[] params) { + if (frameworkMethod.getMethod().getParameterTypes().length != params.length) { + return params; + } + + if (params.length == 0) { + return params; + } + + Object param = params[0]; + if (param == null || !param.getClass().isArray()) { + return new Object[]{params}; + } + + return params; + } + +} diff --git a/src/main/java/junitparams/mappers/BufferedReaderDataMapper.java b/src/main/java/junitparams/mappers/BufferedReaderDataMapper.java new file mode 100644 index 0000000..a7b0fd2 --- /dev/null +++ b/src/main/java/junitparams/mappers/BufferedReaderDataMapper.java @@ -0,0 +1,40 @@ +package junitparams.mappers;
+
+import java.io.BufferedReader;
+import java.io.Reader;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A Data Mapper based on Buffered Reader.
+ */
+class BufferedReaderDataMapper implements DataMapper {
+
+ private final int linesToSkip;
+
+ BufferedReaderDataMapper() {
+ this(0);
+ }
+
+ BufferedReaderDataMapper(int linesToSkip) {
+ this.linesToSkip = linesToSkip;
+ }
+
+ @Override
+ public Object[] map(Reader reader) {
+ BufferedReader br = new BufferedReader(reader);
+ String line;
+ List<String> result = new LinkedList<String>();
+ int lineNo = 0;
+ try {
+ while ((line = br.readLine()) != null) {
+ if (++lineNo > linesToSkip) {
+ result.add(line);
+ }
+ }
+ return result.toArray();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/junitparams/mappers/CsvWithHeaderMapper.java b/src/main/java/junitparams/mappers/CsvWithHeaderMapper.java new file mode 100644 index 0000000..ea4ab59 --- /dev/null +++ b/src/main/java/junitparams/mappers/CsvWithHeaderMapper.java @@ -0,0 +1,19 @@ +package junitparams.mappers; + +import java.io.*; +import java.util.*; + +/** + * Reads a CSV file starting from the second line - the first one is supposed to + * be a header. If you don't want to skip the first line, use @FilePatameters + * without any mapper. + * + * @author Pawel Lipinski + * + */ +public class CsvWithHeaderMapper extends BufferedReaderDataMapper { + + public CsvWithHeaderMapper() { + super(1); + } +} diff --git a/src/main/java/junitparams/mappers/DataMapper.java b/src/main/java/junitparams/mappers/DataMapper.java new file mode 100644 index 0000000..200b03d --- /dev/null +++ b/src/main/java/junitparams/mappers/DataMapper.java @@ -0,0 +1,31 @@ +package junitparams.mappers; + +import java.io.*; + +/** + * Interface to be used by FileParameters'ized test methods. If you want to read + * your own format of data from file, implement the map method appropriately. + * For CSV files, just skip it. + * + * @author Pawel Lipinski + * + */ +public interface DataMapper { + /** + * Maps file contents to parameters. In your implementation read the data + * from the reader. The reader is closed in the framework, so just read it + * :) + * + * While reading transform the data into Object[][], where external + * dimension are different parameter sets, and internal dimension is the set + * of params per single test call + * + * You can optionally return Object[] with Strings inside, but each String + * must be a string in the same format as what you would normally pass to + * @Parameters({}) + * + * @param reader + * @return an array with all parameter sets + */ + Object[] map(Reader reader); +} diff --git a/src/main/java/junitparams/mappers/IdentityMapper.java b/src/main/java/junitparams/mappers/IdentityMapper.java new file mode 100644 index 0000000..14bbf59 --- /dev/null +++ b/src/main/java/junitparams/mappers/IdentityMapper.java @@ -0,0 +1,20 @@ +package junitparams.mappers; + +import java.io.*; +import java.util.*; + +/** + * A mapper, that maps contents of a file to a set of parameters for test + * methods. Basically a CSV with no header and ordering of columns exactly like + * the one in the test methods. + * + * It uses the logic from @Parameters({}) for parsing lines of file, so be sure + * the columns in the file match exactly the ordering of arguments in the test + * method. + * + * @author Pawel Lipinski + * + */ +public class IdentityMapper extends BufferedReaderDataMapper{ + +} diff --git a/src/main/java/junitparams/naming/MacroSubstitutionNamingStrategy.java b/src/main/java/junitparams/naming/MacroSubstitutionNamingStrategy.java new file mode 100644 index 0000000..ce56847 --- /dev/null +++ b/src/main/java/junitparams/naming/MacroSubstitutionNamingStrategy.java @@ -0,0 +1,123 @@ +package junitparams.naming; + +import junitparams.internal.TestMethod; +import junitparams.internal.Utils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Locale; +import java.util.regex.Pattern; + +public class MacroSubstitutionNamingStrategy implements TestCaseNamingStrategy { + private static final String MACRO_PATTERN = "\\{[^\\}]{0,50}\\}"; + // Pattern that keeps delimiters in split result + private static final Pattern MACRO_SPLIT_PATTERN = Pattern.compile(String.format("(?=%s)|(?<=%s)", MACRO_PATTERN, MACRO_PATTERN)); + private static final String MACRO_START = "{"; + private static final String MACRO_END = "}"; + static final String DEFAULT_TEMPLATE = "[{index}] {params} ({method})"; + private TestMethod method; + + public MacroSubstitutionNamingStrategy(TestMethod testMethod) { + this.method = testMethod; + } + + @Override + public String getTestCaseName(int parametersIndex, Object parameters) { + TestCaseName testCaseName = method.getAnnotation(TestCaseName.class); + + String template = getTemplate(testCaseName); + String builtName = buildNameByTemplate(template, parametersIndex, parameters); + + if (builtName.trim().isEmpty()) { + return buildNameByTemplate(DEFAULT_TEMPLATE, parametersIndex, parameters); + } else { + return builtName; + } + } + + private String getTemplate(TestCaseName testCaseName) { + if (testCaseName != null) { + return testCaseName.value(); + } + + return DEFAULT_TEMPLATE; + } + + private String buildNameByTemplate(String template, int parametersIndex, Object parameters) { + StringBuilder nameBuilder = new StringBuilder(); + + String[] parts = MACRO_SPLIT_PATTERN.split(template); + + for (String part : parts) { + String transformedPart = transformPart(part, parametersIndex, parameters); + nameBuilder.append(transformedPart); + } + + return nameBuilder.toString(); + } + + private String transformPart(String part, int parametersIndex, Object parameters) { + if (isMacro(part)) { + return lookupMacroValue(part, parametersIndex, parameters); + } + + return part; + } + + private String lookupMacroValue(String macro, int parametersIndex, Object parameters) { + String macroKey = getMacroKey(macro); + + switch (Macro.parse(macroKey)) { + case INDEX: return String.valueOf(parametersIndex); + case PARAMS: return Utils.stringify(parameters); + case METHOD: return method.name(); + default: return substituteDynamicMacro(macro, macroKey, parameters); + } + } + + private String substituteDynamicMacro(String macro, String macroKey, Object parameters) { + if (isMethodParameterIndex(macroKey)) { + int index = parseIndex(macroKey); + return Utils.getParameterStringByIndexOrEmpty(parameters, index); + } + + return macro; + } + + private boolean isMethodParameterIndex(String macroKey) { + return macroKey.matches("\\d+"); + } + + private int parseIndex(String macroKey) { + return Integer.parseInt(macroKey); + } + + private String getMacroKey(String macro) { + return macro + .substring(MACRO_START.length(), macro.length() - MACRO_END.length()) + .toUpperCase(Locale.ENGLISH); + } + + private boolean isMacro(String part) { + return part.startsWith(MACRO_START) && part.endsWith(MACRO_END); + } + + private enum Macro { + INDEX, + PARAMS, + METHOD, + NONE; + + public static Macro parse(String value) { + if (macros.contains(value)) { + return Macro.valueOf(value); + } else { + return Macro.NONE; + } + } + + private static final HashSet<String> macros = new HashSet<String>(Arrays.asList( + Macro.INDEX.toString(), Macro.PARAMS.toString(), Macro.METHOD.toString()) + ); + } +} diff --git a/src/main/java/junitparams/naming/TestCaseName.java b/src/main/java/junitparams/naming/TestCaseName.java new file mode 100644 index 0000000..4063f1a --- /dev/null +++ b/src/main/java/junitparams/naming/TestCaseName.java @@ -0,0 +1,40 @@ +package junitparams.naming; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Use this annotation to specify the name for individual test case. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface TestCaseName { + + /** + * A template of the individual test case name. + * This template can contain macros, which will be substituted by their actual values at runtime. + * <p> + * Supported macros are: + * <ul> + * <li><b>{index}</b> - index of parameters set (starts from zero). Hint: use it to avoid names duplication.</li> + * <li><b>{params}</b> - parameters set joined by comma.</li> + * <li><b>{method}</b> - testing method name.</li> + * <li> + * <b>{0}</b>, <b>{1}</b>, <b>{2}</b> - single parameter by index in current parameters set. + * If there is no parameter with such index, it will use empty string. + * </li> + * </ul> + * Lets assume, that we are testing Fibonacci sequence generator. We have a test with the following signature + * <pre><code> + * {@literal @}Parameters({ "0,1", "8,34" }) + * public void testFibonacci(int indexInSequence, int expectedNumber) { ... } + * </code></pre> + * Here are some examples, that can be used as a test name template: + * <ul> + * <li>{method}({params}) => testFibonacci(0, 1), testFibonacci(8, 34)</li> + * <li>fibonacci({0}) = {1} => fibonacci(0) = 1, fibonacci(8) = 34</li> + * <li>{0} element should be {1} => 0 element should be 1, 8 element should be 34</li> + * <li>Fibonacci sequence test #{index} => Fibonacci sequence test #0, Fibonacci sequence test #1</li> + * </ul> + */ + String value() default MacroSubstitutionNamingStrategy.DEFAULT_TEMPLATE; +}
\ No newline at end of file diff --git a/src/main/java/junitparams/naming/TestCaseNamingStrategy.java b/src/main/java/junitparams/naming/TestCaseNamingStrategy.java new file mode 100644 index 0000000..478b96c --- /dev/null +++ b/src/main/java/junitparams/naming/TestCaseNamingStrategy.java @@ -0,0 +1,8 @@ +package junitparams.naming; + +/** + * A strategy that can resolve a test case method name by it's parameters. + */ +public interface TestCaseNamingStrategy { + String getTestCaseName(int parametersIndex, Object parameters); +} |