Tuesday, December 01, 2020

Parameterized Tests in JUnit 5

A parameterized test allows you to run a test against a varying set of data. If you find yourself calling the same test but with different inputs, over and over again, a parameterized test would help make your code cleaner. To create one in JUnit 5 you need to:

  • Annotate the test method with @ParameterizedTest
  • Annotate the test method with at lease one source e.g. @ValueSource
  • Consume the arguments in the test method

The sections below describe some of the commonly used source annotations you can use to provide inputs to your test methods.

@ValueSource
This annotation lets you specify a single array of literal values that will be passed to your test method one by one, as shown in the example below:

@ParameterizedTest
@ValueSource(ints = {2, 4, 6})
void testIsEven(final int i) {
  assertTrue(i % 2 == 0);
}

@CsvSource
This annotation allows you to specify an array of comma-separated values, which is useful if your test method takes multiple arguments. If you have a large number of arguments, you can use an ArgumentsAccessor to extract the arguments as opposed to creating a method with a long parameter list. For example:

@ParameterizedTest(name = "Person with name {0} and age {1}")
@CsvSource({ "Alice, 28",
             "Bob, 30" })
void testPerson(final String name, final int age) {
  final Person p = new Person(name, age);
  assertThat(p.getName(), is(name));
  assertThat(p.getAge(), is(age));
}

@ParameterizedTest(name = "Person with name {0} and age {1}")
@CsvSource({ "Alice, 28",
             "Bob, 30" })
void testPersonWithArgumentAccessor(final ArgumentsAccessor arguments) {
  final String name = arguments.getString(0);
  final int age = arguments.getInteger(1);
  final Person p = new Person(name, age);
  assertThat(p.getName(), is(name));
  assertThat(p.getAge(), is(age));
}

By the way, note how I have also customised the display name of the test using the {0} and {1} argument placeholders.

@CsvFileSource
This annotation is similar to CsvSource but allows you to load your test inputs from a CSV file on the classpath. For example:

@ParameterizedTest(name = "Person with name {0} and age {1}")
@CsvFileSource(resources = { "data.csv" })
void testPerson(final String name, final int age) {
  final Person p = new Person(name, age);
  assertThat(p.getName(), is(name));
  assertThat(p.getAge(), is(age));
}

@MethodSource
This annotation allows you to specify a factory method which returns a stream of objects to be passed to your test method. If your test method has multiple arguments, your factory method should return a stream of Arguments instances as shown in the example below:

import static org.junit.jupiter.params.provider.Arguments.*;

@ParameterizedTest(name = "{0} is sorted to {1}")
@MethodSource("dataProvider")
void testSort(final int[] input, final int[] expected) {
  Arrays.sort(input);
  assertArrayEquals(expected, input);
}

static Stream<Arguments> dataProvider() {
  return Stream.of(
      arguments(new int[] { 1, 2, 3 }, new int[] { 1, 2, 3 }),
      arguments(new int[] { 3, 2, 1 }, new int[] { 1, 2, 3 }),
      arguments(new int[] { 5, 5, 5 }, new int[] { 5, 5, 5 }));
}

For more information, see the JUnit 5 User Guide on Parameterized Tests.

If you're still on JUnit 4 (why?!), check out my previous post on Parameterized Tests in JUnit 4.