12 November 2016

JUnit 5 introduces dynamic test creation: A method annotated with @TestFactory is expected to return some kind of iterable ((e.g. List, Stream or Iterable) of type DynamicTest. This can be used to create random input data for a test.

  @TestFactory
  Stream<DynamicTest> dynamicTestCreation() {
    return new Random().ints(3, 0, 5).mapToObj(
      n -> dynamicTest("test " + n, () -> assertTrue(n < 5))
    );
  }

But dynamic tests are also a way to write parameterized tests. With this post I like to to compare the different ways of writing parameterized tests with JUnit.

JUnit 4 ParameterizedTest

JUnit 4.0 exposed an API for running tests, so that third parties gain the possibility to run tests different to the default behaviour. As an example the JUnit team provided ParameterizedTest, a runner to parameterize the same test code with different data.

@RunWith(Parameterized.class)
public class JUnit4ParameterizedTest {

  @Parameters
  public static Collection<Object[]> data() {
    return Arrays.asList(new Object[][]{
      {17, false},
      {22, true}
    });
  }

  @Parameter(0)
  public int age;

  @Parameter(1)
  public boolean isAdult;

  @Test
  public void personIsAdult() {
    assertThat(new Person(age).isAdult(), is(isAdult));
  }
}

As said, ParameterizedTest was provided as an example and never intended to be part of the supported functionality. Although users of JUnit took this as the way to do it.

JUnitParams

JUnits own ParameterizedTest runner has some shortcomings. That’s why JUnitParams has been created. JUnitParams provides the JUnitParamsRunner and the @Parameter annotation to mark the test methods which needs to be parameterized.

@RunWith(JUnitParamsRunner.class)
public class JUnitParamsTest {

  private List<Object[]> parametersForPersonIsAdult() {
    return Arrays.asList(new Object[][]{
      {17, false},
      {22, true}
    });
  }

  @Test
  @Parameters
  public void personIsAdult(final int age, final boolean valid) {
    assertThat(new Person(age).isAdult(), is(valid));
  }
}

As you can easyily spot, JUnitParams provides types safety for the injected data and convenience methods for data generation. It’s worth to have a look at the JUnitParams usage samples to see all the possibilities JUnitParams offers to provide data to test methods.

JUnit 5 Dynamic Tests

ParameterizedTest is no longer supporeted by JUnit 5, but as shown earlier, JUnit 5 introduces dynamic test creation which can also be used to inject data into test runs.

class JUnit5DynamicTestTest {

  private final List<Object[]> data = Arrays.asList(new Object[][]{
    {17, false},
    {21, true}
  });

  @TestFactory
  Stream<DynamicTest> personIsAdult() {
    return data.stream().map(data -> {
        final int age = (int) data[0];
        final boolean isAdult = (boolean) data[1];
        return dynamicTest(age + " -> " + isAdult, () -> assertThat(new Person(age).isAdult(), is(isAdult)));
      }
    );
  }
}

Due to the missing direct support of parameterized tests, we have to map the data ourselves including those nasty casts.

Conclusion

JUnit 5s dynamic tests are no valid substitute for JUnit 4s ParameterizedTest. Let’s hope that JUnitParams fill the gap (again). As of this writing there is nothing planned yet.