Parameterized Tests with JUnit 5 Jupiter

This post has originally been published on the codecentric blog. It was translated to english and slightly edited for this blog.

A month back the JUnit team published the offical version of JUnit 5. There’s plenty of resources for high level overviews and if you want to catch up I can recommend this (german) article on the codecentric blog by my colleague Tobias Trelle. In this post I want to have a closer look at how JUnit 5’s test engine Jupiter enables Parameterized Tests. To fully appreciate the new possibilities, let’s have a brief look what we had to do up until now:

@RunWith(Parameterized.class)
public class FibonacciTest {
 
  @Parameters public static Collection<Object[]> data() {
    return Arrays.asList(new Object[][] {
      {0,0},{1,1},{2,1},{3,2} })};
 
  private int input, expected;
 
  public FibonacciTest(int input, int expected) {
    this.input = input; this.expected = expected;
  }
 
  @Test
  public void test() {
    assertEquals(expected, Fibonacci.compute(input));
  }
}

JUnit 4 contained a test runner named Parameterized, which would take parameters from a method annotated with @Parameters and use them, when creating instances of a test class. You could then use those constructor parameters from your tests. Looking at the example above you probably can imagine, that these test quickly became confusing and hard to maintain. On top of that most of the time it didn’t make much sense to have more than one test method in every parameterized test class.
Other alternatives like JUnitParams took a different approach and made those tests a bit easier by moving the definition of parameters nearer to the respective test. Still, those tests frequently were harder to grasp then necessary:

@RunWith(JUnitParamsRunner.class)
public class PersonTest {
 
  @Test
  @Parameters({"0, 0", "1, 1", "2, 1", "3, 2" })
  public void personIsAdult(int input, int expected) {
    assertEquals(expected, Fibonacci.compute(input));
  }
 
}

Both approaches are based on the usage of test runners. Since we could only specify one test runner for every test class there was no way to combine functionality from different test runners. For example, if we wanted to use the HierarchicalContextRunner to structure our parameterized tests hierarchically we found ourselves between a rock and a hard spot.

Parameterized tests with JUnit 5

Using Jupiter you can approach parameterized tests in two different ways: Dynamic Tests were introduced pretty early in the development phase and they are one way to create parameterized tests. Additionally there’s a feature that’s actually called Parameterized Tests, it appeared later with milestone 4.

Dynamic Tests

Generally, we specify our tests statically. We implement a test method and annotate it with @Test. JUnit then discovers and executes those tests. Dynamic tests work differently:

class DynamicTests {
 
  @TestFactory
  List<DynamicTest> createSomeTests() {
 
    return Arrays.asList(
      DynamicTest.dynamicTest("First dynamically created test",
        () -> assertTrue(true)),
 
      DynamicTest.dynamicTest("Second dynamically created test",
        () -> assertTrue(true))
    );
  }
}

In contrast to static tests we have to implement a method that returns a collection, an iterable or a stream of type DynamicTest. This method has to be annotated with @TestFactory. You can use a static method called dynamicTest() to create a dynamic test instance using a display name (see @DisplayName) as its first parameter and a lambda containing the test code to be executed as its second parameter. A @TestFactory method can also return DynamicContainer collections as a dynamic equivalent of a static test class.

One way to use dynamic tests would be to have logic in the factory method that decides upfront if certain tests are to be generated or not. In comparison, static tests always exist at runtime and you would have to use Assumptions or Execution conditions to abort test execution.

You can also use dynamic tests to create tests from a data source, though. I imagine this could be an option if a lot of logic would be involved (e.g. downloading a file provided by a business unit and transforming the contained data before creating tests from it).

Parameterized tests

However, it’s easier to create parameterized tests using the annotation @ParameterizedTest. You’ll use it instead of @Test und it has to be accompanied by a second annotation that connects the source of the parameters, an ArgumentsProvider:

class ParameterizedTests {
 
  @ParameterizedTest
  @ValueSource(ints = {1,2,3,4,5})
  void valueSourceTest(int param){
    // ...
  }
 
}

The values returned by an ArgumentsProvider have to match the type of the method parameter. Jupiter comes with a set of predefined ArgumentProviders with their respective annotations: In the example above I’m using @ValueSource, you can use its attributes to define static values that should be passed to your test method. @CsvSource and @CsvFileSource enable the use of csv style data and @EnumSource passes the values of an enumeration to a test. With @MethodSource you can connect an arbitrary (static) method as a source for test parameters.

Implementing a custom ArgumentsProvider

This concept of parameterized tests is similar to the JUnitParams approach. Still it’s a lot more flexible only looking at the different providers that Jupiter has on board. It gets even more interesting because you can implement your own ArgumentsProvider. For example, an implementation that enables the use of Json data could look like this:

@ArgumentsSource(JsonArgumentsProvider.class)
public @interface JsonSource {
 
  String[] value();
  Class<?> type();
}

Using the annotation @JsonSource we can define an array of string values, each containing some json data. The type attribute is used to define a target type for deserialisation. Using the @ArgumentsSource annotation an @ArgumentsProvider is connected:

public class JsonArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<JsonSource> {
 
  private String[] values;
  private Class<?> type;
 
  @Override
  public void accept(final JsonSource annotation) {
    values = annotation.value();
    type = annotation.type();
  }
 
  @Override
  public Stream<? extends Arguments> provideArguments(final ExtensionContext context) {
    return Arrays.stream(values)
        .map(value -> new Gson().fromJson(value, type))
        .map(Arguments::of);
  }
}

The @AnnotationConsumer interface requires us to implement the accept() method, we use it to access the attribute values of the @JsonSource annotation and copy thos to the provider instance. The provideArguments() method is implemented for the ArgumentsProvider interface, it deserializes the values to the target type using the Gson library. In our tests, we can now use our annotation as usual:

@ParameterizedTest
@JsonSource(value = "{firstname:'Jane', lastname: 'Doe'}", type = Person.class)
void jsonSourceTest(Person param) {
  System.out.println(param);
}

Conclusion

Many projects can benefit from parameterized tests. The JUnit 4 facilities have been inflexible and resulting tests have been hard to maintain. The JUnit 5 Jupiter test engine brings many new possibilities. For most cases, @ParameterizedTest will be an easy way to implement our parameterized tests and thanks to the completely new extension model we can also realize specialized use cases like nested, parameterized tests. For complex scenarios dynamic tests might be a reasonable alternative.

The source code examples from this article are available on Github, for further information I recommend the extensive official documentation of the project. JUnit 5 has just been published and I’m eager to see how it will resonate in our day-to-day projects.

Leave a Reply