Skip to content
Advertisement

How to implement JUnit 4 parameterized tests in JUnit 5?

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 a Stream, 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 become Stream<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 annotation JavaTimeConversionPattern.

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);
    }

}
User contributions licensed under: CC BY-SA
8 People found this is helpful
Advertisement