In JUnit 4 it was easy to test invariants across a bunch of classes by using the @Parameterized
annotation. The key thing is that a collection of tests are being run against a single list of arguments.
How to replicate this in JUnit 5, without using JUnit-vintage?
@ParameterizedTest
is not applicable to a test class. @TestTemplate
sounded like it might be appropriate, but that annotation’s target is also a method.
An example of such a JUnit 4 test is:
@RunWith( Parameterized.class ) public class FooInvariantsTest{ @Parameterized.Parameters public static Collection<Object[]> data(){ return new Arrays.asList( new Object[]{ new CsvFoo() ), new Object[]{ new SqlFoo() ), new Object[]{ new XmlFoo() ), ); } private Foo fooUnderTest; public FooInvariantsTest( Foo fooToTest ){ fooUnderTest = fooToTest; } @Test public void testInvariant1(){ ... } @Test public void testInvariant2(){ ... } }
Advertisement
Answer
The parameterized test feature in JUnit 5 doesn’t provide the exact same features than those provided by JUnit 4.
New features with more flexibility were introduced… but it also lost the JUnit4 feature where the parameterized test class uses the parameterized fixtures/assertions at the class level that is for all test methods of the class.
Defining @ParameterizedTest
for each test method by specifying the “input” is so needed.
Beyond that lack I will present the main differences between the 2 versions and how to use parameterized tests in JUnit 5.
TL;DR
To write a parameterized test that specifies a value by case to test as your in your question,
org.junit.jupiter.params.provider.MethodSource
should do the job.
@MethodSource
allows you to refer to one or more methods of the test class. Each method must return aStream
,Iterable
,Iterator
, or array of arguments. In addition, each method must not accept any arguments. By default such methods must be static unless the test class is annotated with@TestInstance(Lifecycle.PER_CLASS)
.If you only need a single parameter, you can return instances of the parameter type directly as demonstrated by the following example.
As JUnit 4, @MethodSource
relies on a factory method and may also be used for test methods that specify multiple arguments.
In JUnit 5, it is the way of writing parameterized tests the closest to JUnit 4.
JUnit 4 :
@Parameters public static Collection<Object[]> data() {
JUnit 5 :
private static Stream<Arguments> data() {
Main improvements :
Collection<Object[]>
is becomeStream<Arguments>
that provides more flexibility.the way of binding the factory method to the test method differs a little.
It is now shorter and less error prone : no more requirement to create a constructor and declares field to set the value of each parameter. The binding of the source is done directly on the parameters of the test method.With JUnit 4, inside a same class, one and only one factory method has to be declared with
@Parameters
.
With JUnit 5, this limitation is lifted : multiple methods may indeed be used as factory method.
So, inside the class, we can so declare some test methods annotated with@MethodSource("..")
that refer different factory methods.
For example here is a sample test class that asserts some addition computations :
import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.api.Assertions; public class ParameterizedMethodSourceWithArgumentsTest { @ParameterizedTest @MethodSource("addFixture") void add(int a, int b, int result) { Assertions.assertEquals(result, a + b); } private static Stream<Arguments> addFixture() { return Stream.of( Arguments.of(1, 2, 3), Arguments.of(4, -4, 0), Arguments.of(-3, -3, -6)); } }
To upgrade existing parameterized tests from JUnit 4 to JUnit 5, @MethodSource
is a candidate to consider.
Summarize
@MethodSource
has some strengths but also some weaknesses.
New ways to specify sources of the parameterized tests were introduced in JUnit 5.
Here some additional information (far being exhaustive) about them that I hope could give a broad idea on how deal with in a general way.
Introduction
JUnit 5 introduces parameterized tests feature in these terms :
Parameterized tests make it possible to run a test multiple times with different arguments. They are declared just like regular
@Test
methods but use the@ParameterizedTest
annotation instead. In addition, you must declare at least one source that will provide the arguments for each invocation.
Dependency requirement
Parameterized tests feature is not included in the junit-jupiter-engine
core dependency.
You should add a specific dependency to use it : junit-jupiter-params
.
If you use Maven, this is the dependency to declare :
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>5.0.0</version> <scope>test</scope> </dependency>
Sources available to create data
Contrary to JUnit 4, JUnit 5 provides multiple flavors and artifacts to write parameterized tests
The ways to favor depend generally on the source of data you want to use.
Here are the source types proposed by the framework and described in the documentation :
@ValueSource
@EnumSource
@MethodSource
@CsvSource
@CsvFileSource
@ArgumentsSource
Here are the 3 main sources I actually use with JUnit 5 and I will present:
@MethodSource
@ValueSource
@CsvSource
I consider them as basic as I write parameterized tests. They should allow to write in JUnit 5, the type of JUnit 4 tests that you described.
@EnumSource
, @ArgumentsSource
and @CsvFileSource
may of course be helpful but they are more specialized.
Presentation of @MethodSource
, @ValueSource
and @CsvSource
1) @MethodSource
This type of source requires to define a factory method.
But it also provides much flexibility.
In JUnit 5, it is the way of writing parameterized tests the closest to JUnit 4.
If you have a single method parameter in the test method and you want to use any type as source, @MethodSource
is a very good candidate.
To achieve it, define a method that returns a Stream of the value for each case and annotate the test method with @MethodSource("methodName")
where methodName
is the name of this data source method.
For example, you could write :
import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; public class ParameterizedMethodSourceTest { @ParameterizedTest @MethodSource("getValue_is_never_null_fixture") void getValue_is_never_null(Foo foo) { Assertions.assertNotNull(foo.getValue()); } private static Stream<Foo> getValue_is_never_null_fixture() { return Stream.of(new CsvFoo(), new SqlFoo(), new XmlFoo()); } }
If you have multiple method parameters in the test method and you want to use any type as source, @MethodSource
is also a very good candidate.
To achieve it, define a method that returns a Stream of org.junit.jupiter.params.provider.Arguments
for each case to test.
For example, you could write :
import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.api.Assertions; public class ParameterizedMethodSourceWithArgumentsTest { @ParameterizedTest @MethodSource("getFormatFixture") void getFormat(Foo foo, String extension) { Assertions.assertEquals(extension, foo.getExtension()); } private static Stream<Arguments> getFormatFixture() { return Stream.of( Arguments.of(new SqlFoo(), ".sql"), Arguments.of(new CsvFoo(), ".csv"), Arguments.of(new XmlFoo(), ".xml")); } }
2)@ValueSource
If you have a single method parameter in the test method and you may represent the source of the parameter from one of these built-in types (String, int, long, double), @ValueSource
suits.
@ValueSource
defines indeed these attributes :
String[] strings() default {}; int[] ints() default {}; long[] longs() default {}; double[] doubles() default {};
You could for example use it in this way :
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; public class ParameterizedValueSourceTest { @ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) void sillyTestWithValueSource(int argument) { Assertions.assertNotNull(argument); } }
Beware 1) you must not specify more than one annotation attribute.
Beware 2) The mapping between the source and the parameter of the method can be done between two distinct types.
The type String
used as source of data allows particularly, thanks to its parsing, to be converted into multiple other types.
3) @CsvSource
If you have multiple method parameters in the test method, a @CsvSource
may suit.
To use it, annotate the test with @CsvSource
and specify in a array of String
each case.
Values of each case are separated by a comma.
Like @ValueSource
, the mapping between the source and the parameter of the method can be done between two distinct types.
Here is an example that illustrates that :
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; public class ParameterizedCsvSourceTest { @ParameterizedTest @CsvSource({ "12,3,4", "12,2,6" }) public void divideTest(int n, int d, int q) { Assertions.assertEquals(q, n / d); } }
@CsvSource
VS @MethodSource
These source types serve a very classic requirement : mapping from the source to multiple method parameters in the test method.
But their approach is different.
@CsvSource
has some advantages : it is clearer and shorter.
Indeed, parameters are defined just above the tested method, no requirement to create a fixture method that may in addition generate “unused” warnings.
But it also has an important limitation concerning mapping types.
You have to provide an array of String
. The framework provides conversion features but it is limited.
To summarize, while the String
provided as source and the parameters of the test method have the same type (String
->String
) or rely on built-in conversion (String
->int
for example), @CsvSource
appears as the way to use.
As it is not the case, you have to make a choice between
keeping the flexibility of @CsvSource
by creating a custom converter (ArgumentConverter
subclass) for conversions not performed by the framework or using @MethodSource
with a factory method that
returns Stream<Arguments>
.
It has the drawbacks described above but it also has the great benefit to map out-of-the box any type from the source to the parameters.
Argument Conversion
About the mapping between the source (@CsvSource
or @ValueSource
for example) and the parameters of the test method, as seen, the framework allows to do some conversions if the types are not the same.
Here is a presentation of the two types of conversions :
3.13.3. Argument Conversion
Implicit Conversion
To support use cases like
@CsvSource
, JUnit Jupiter provides a number of built-in implicit type converters. The conversion process depends on the declared type of each method parameter.…..
String
instances are currently implicitly converted to the following target types.Target Type | Example boolean/Boolean | "true" → true byte/Byte | "1" → (byte) 1 char/Character | "o" → 'o' short/Short | "1" → (short) 1 int/Integer | "1" → 1 .....
For example in the previous example, an implicit conversion is done between String
from source and int
defined as parameter:
@CsvSource({ "12,3,4", "12,2,6" }) public void divideTest(int n, int d, int q) { Assertions.assertEquals(q, n / d); }
And here, an implicit conversion is done from String
source to LocalDate
parameter:
@ParameterizedTest @ValueSource(strings = { "2018-01-01", "2018-02-01", "2018-03-01" }) void testWithValueSource(LocalDate date) { Assertions.assertTrue(date.getYear() == 2018); }
If for two types, no conversion is provided by the framework,
which is the case for custom types, you should use an ArgumentConverter
.
Explicit Conversion
Instead of using implicit argument conversion you may explicitly specify an
ArgumentConverter
to use for a certain parameter using the@ConvertWith
annotation like in the following example.
JUnit provides a reference implementation for clients who need to create a specific ArgumentConverter
.
Explicit argument converters are meant to be implemented by test authors. Thus, junit-jupiter-params only provides a single explicit argument converter that may also serve as a reference implementation:
JavaTimeArgumentConverter
. It is used via the composed annotationJavaTimeConversionPattern
.
Test method using this converter :
@ParameterizedTest @ValueSource(strings = { "01.01.2017", "31.12.2017" }) void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) { assertEquals(2017, argument.getYear()); }
JavaTimeArgumentConverter
converter class :
package org.junit.jupiter.params.converter; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.OffsetTime; import java.time.Year; import java.time.YearMonth; import java.time.ZonedDateTime; import java.time.chrono.ChronoLocalDate; import java.time.chrono.ChronoLocalDateTime; import java.time.chrono.ChronoZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalQuery; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.params.support.AnnotationConsumer; /** * @since 5.0 */ class JavaTimeArgumentConverter extends SimpleArgumentConverter implements AnnotationConsumer<JavaTimeConversionPattern> { private static final Map<Class<?>, TemporalQuery<?>> TEMPORAL_QUERIES; static { Map<Class<?>, TemporalQuery<?>> queries = new LinkedHashMap<>(); queries.put(ChronoLocalDate.class, ChronoLocalDate::from); queries.put(ChronoLocalDateTime.class, ChronoLocalDateTime::from); queries.put(ChronoZonedDateTime.class, ChronoZonedDateTime::from); queries.put(LocalDate.class, LocalDate::from); queries.put(LocalDateTime.class, LocalDateTime::from); queries.put(LocalTime.class, LocalTime::from); queries.put(OffsetDateTime.class, OffsetDateTime::from); queries.put(OffsetTime.class, OffsetTime::from); queries.put(Year.class, Year::from); queries.put(YearMonth.class, YearMonth::from); queries.put(ZonedDateTime.class, ZonedDateTime::from); TEMPORAL_QUERIES = Collections.unmodifiableMap(queries); } private String pattern; @Override public void accept(JavaTimeConversionPattern annotation) { pattern = annotation.value(); } @Override public Object convert(Object input, Class<?> targetClass) throws ArgumentConversionException { if (!TEMPORAL_QUERIES.containsKey(targetClass)) { throw new ArgumentConversionException("Cannot convert to " + targetClass.getName() + ": " + input); } DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); TemporalQuery<?> temporalQuery = TEMPORAL_QUERIES.get(targetClass); return formatter.parse(input.toString(), temporalQuery); } }