How to implement JUnit 4 parameterized tests in JUnit 5?

In JUnit 4, it was easy to check invariants in a class group using the @Parameterized annotation. The main thing is that a collection of tests runs against a single argument list.

How to replicate this in JUnit 5 without using JUnit-vintage?

@ParameterizedTest not applicable to the test class. @TestTemplate sounds like it might be appropriate, but the purpose of the annotation 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(){ ... } } 
+5
source share
1 answer

TL DR

In order to write a parameterized test that sets a value in each case, in order to check it in your question, org.junit.jupiter.params.provider.MethodSource should do the job.

@MethodSource allows you to reference one or more class test methods. Each method should return a Stream , Iterable , Iterator or an array of arguments. In addition, each method should not accept any arguments. By default, such methods should be static, unless the test class is annotated using @TestInstance(Lifecycle.PER_CLASS) .

If you need only one parameter, you can return instances as shown in the following example.

Like JUnit 4, @MethodSource relies on the factory method and can also be used for test methods that specify multiple arguments.

In JUnit 5, this is a way of writing parameterized tests closest to JUnit 4.

JUnit 4:

 @Parameters public static Collection<Object[]> data() { 

JUnit 5:

 private static Stream<Arguments> data() { 

Major improvements:

  • Collection<Object[]> becomes Stream<Arguments> , which provides more flexibility.

  • The way the factory method is bound to the test method is slightly different.
    Now it is shorter and less error prone: you no longer need to create a constructor and declares a field to set the value of each parameter. Source binding is performed directly by the parameters of the test method.

  • With JUnit 4, inside one class, one and only one factory method must be declared using @Parameters .
    With JUnit 5, this restriction is removed: several methods can indeed be used as a factory method.
    Thus, inside the class, we can declare some testing methods annotated with @MethodSource("..") that refer to different factory methods.

For example, an example test class that claims some additional calculations:

 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 for review.


Summarize

@MethodSource has some strengths, but also some disadvantages.
New ways to specify the sources of parameterized tests were introduced in JUnit 5.
Here is some additional information (far from exhaustive) about them, which, I hope, could give a wide idea of ​​how things are going in general.

Introduction

JUnit 5 introduces parameterized test functions in these terms:

Parameterized tests allow you to run a test several times with different arguments. They are declared like regular @Test methods @Test but use the @ParameterizedTest annotation @ParameterizedTest . In addition, you must declare at least one source that will provide arguments for each call.

Dependency requirement

The function of parameterized tests is not included in the junit-jupiter-engine kernel dependency.
You must add a specific dependency to use it: junit-jupiter-params .

If you are using Maven, this is the dependency for the declaration:

 <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>5.0.0</version> <scope>test</scope> </dependency> 

Sources available for creating data

Unlike JUnit 4, JUnit 5 provides multiple flavors and artifacts for recording parameterized tests.
Promotion methods depend mainly on the data source that you want to use.

Here are the types of sources suggested by the framework and described in the documentation :

  • @ValueSource
  • @EnumSource
  • @MethodSource
  • @CsvSource
  • @CsvFileSource
  • @ArgumentsSource

Here are 3 main sources that I use with JUnit 5, and I will introduce:

  • @MethodSource
  • @ValueSource
  • @CsvSource

I consider them to be the main ones, as I write parameterized tests. They should be allowed to write in JUnit 5, the type of JUnit 4 test you describe.
@EnumSource , @ArgumentsSource and @CsvFileSource may be useful, but they are more specialized.

Presentation of @MethodSource , @ValueSource and @CsvSource

1) @MethodSource

This type of source requires a factory method definition.
But it also provides more flexibility.

In JUnit 5, this is a way of writing parameterized tests closest to JUnit 4.

If you have a single method parameter in a test method, and you want to use any type as a source, @MethodSource is a very good candidate.
To achieve this, define a method that returns a stream of values ​​for each case and annotates the test method using @MethodSource("methodName") , where methodName is the name of this data source method.

For example, you can 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 several method parameters in a test method, and you want to use any type as a source, @MethodSource also a very good candidate.
To achieve this, define a method that returns the org.junit.jupiter.params.provider.Arguments stream for each case to be verified.

For example, you can 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 a test method, and you can represent the parameter source from one of these built-in types (String, int, long, double) , @ValueSource .

@ValueSource really defines these attributes:

 String[] strings() default {}; int[] ints() default {}; long[] longs() default {}; double[] doubles() default {}; 

You can, for example, use it as follows:

 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 should not specify more than one annotation attribute.
Beware 2) The mapping between the source and the method parameter can be between two different types.
The String type used as a data source allows, in particular, to parse it into several other types thanks to its parsing.

3) @CsvSource

If a test method has several method parameters , @CsvSource may be required.
To use it, annotate the test with @CsvSource and specify each case in the String array.
The values ​​of each case are separated by a comma.

Like @ValueSource , a mapping between a source and a method parameter can be performed between two different 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 types of sources serve a very classic requirement: mapping from a source to several method parameters in a test method.
But their approach is different.

@CsvSource has some advantages: it becomes clearer and shorter.
Indeed, the parameters are defined just above the tested method; the creation of a binding method that can additionally generate β€œunused” warnings is not required.
But this also has an important limitation on the types of mappings.
You must provide an array of String . The structure provides transformation capabilities, but it is limited.

To summarize, while the String provided as the source and the parameters of the test method are of the same type ( String β†’ String ) or rely on the built-in conversion ( String β†’ int for example), @CsvSource appears as a way to use it.

As it’s not, you need to choose between retaining the flexibility of @CsvSource by creating a custom converter (a subclass of ArgumentConverter ) for transformations not performed by the framework or using @MethodSource using the factory method that returns Stream<Arguments> .
This has the drawbacks described above, but it also has a great advantage for displaying outside the box of any type from source to parameters.

Converting Arguments

On the mapping between the source ( @CsvSource or @ValueSource for example) and the parameters of the test method, as you can see, the structure allows you to make some conversions if the types do not match.

Here is a representation of two types of transforms:

3.13.3. Converting Arguments

Implicit conversion

To support use cases such as @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 performed between String from the source and int , defined as a parameter:

 @CsvSource({ "12,3,4", "12,2,6" }) public void divideTest(int n, int d, int q) { Assertions.assertEquals(q, n / d); } 

And here the implicit conversion is done from the String source to LocalDate :

 @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 the conversion is not provided by the framework used for custom types, you should use ArgumentConverter .

Explicit conversion

Instead of using implicit argument conversion, you can explicitly specify ArgumentConverter to use for a specific parameter using @ConvertWith , as in the following example.

JUnit provides a reference implementation for clients who need to create a specific ArgumentConverter .

Explicit argument converters should be implemented using test authors. Thus, junit-jupiter-params provides only one explicit argument converter, which can also serve as a reference implementation: JavaTimeArgumentConverter . It is used through compiled annotation JavaTimeConversionPattern .

Testing 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); } } 
+7
source

Source: https://habr.com/ru/post/1272835/


All Articles