JUnit 5 @ParameterizedTest

1. Setup

Include junit-jupiter-params dependency in order to use parameterized tests.

Find the latest version at this link.

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>${junit-version}</version>
    <scope>test</scope>
</dependency>

2. @ParameterizedTest

  • Use @ParameterizedTest annotation to execute a test multiple times but with different arguments.
    We do not need to use @Test annotation, instead only @ParameterizedTest annotation is used on such tests.
  • We must declare at least one argument source that will provide the arguments for each invocation which will be consumed in the test method.
  • In the given example, testPalindrome will be invoked 2 times for each string in the @ValueSource annotation. We van access the argument using the method parameter word.
  • Use name argument in the @ParameterizedTest annotation to customize the display message.
public class ParameterizedTests 
{
	public boolean isPalindrome(String s) {
		return s == null ? false : StringUtils.reverse(s).equals(s);
	}
	
	@ParameterizedTest(name = "{index} - {0} is a palindrome")
	@ValueSource(strings = { "12321", "pop" })
	void testPalindrome(String word) {
	    assertTrue(isPalindrome(word));
	}
}

3. Test arguments sources

There are several ways we can pass the arguments to the test method. Let’s explore them.

3.1. @ValueSource

  • Use @ValueSource for simple literal values such as primitives and strings.
  • It specifies a single array of values and can only be used for providing a single argument per parameterized test invocation.
  • Java supports autoboxing so we can consume the literals in their wrapper classes as well.
  • We can not pass null as an argument, even for String and Class types.
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testMethod(int argument) {
    //test code
}

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testMethodWithAutoboxing(Integer argument) {
    //test code
}

3.2. @NullSource

It provides a single null argument to the annotated @ParameterizedTest method.

@ParameterizedTest
@NullSource
void testMethodNullSource(Integer argument) {
	assertTrue(argument == null);
}

3.3. @EmptySource

It provides a single empty argument to the annotated @ParameterizedTest method of the following types:

  • java.lang.String
  • java.util.List
  • java.util.Set
  • java.util.Map
  • primitive arrays (e.g. int[])
  • object arrays (e.g. String[])
@ParameterizedTest
@EmptySource
void testMethodEmptySource(String argument) {
	assertTrue(StringUtils.isEmpty(argument));
}

3.4. @NullAndEmptySource

It combines the functionality of @NullSource and @EmptySource. In the given example, the test method will be invoked two times – first with a null value and then with an empty value.

@ParameterizedTest
@NullAndEmptySource
void testMethodNullAndEmptySource(String argument) {
	assertTrue(StringUtils.isEmpty(argument));
}

Testing null and non-null values in a single test

We already know that @ValueSource annotation does not support the null as a value.

So by using @NullSource and @EmptySource with @ValueSource, we can test null, non-null and empty values, all in the same test.

3.5. @EnumSource

It provides a convenient way to use Enum constants. The test method will be invoked for each enum constant at a time.

In the given example, the test method will be invoked 4 times, once for each enum constant.

enum Direction {
	EAST, WEST, NORTH, SOUTH
}

@ParameterizedTest
@EnumSource(Direction.class)
void testWithEnumSource(Direction d) {
    assertNotNull(d);
}

3.6. @MethodSource

  • It is used to refer to one or more factory methods of the test class or external classes. The factory method must generate a stream of arguments, where each argument within the stream will be consumed by the annotated @ParameterizedTest method.
  • The factory method must be static, unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).
  • Also, the factory method must not take any method argument.
@ParameterizedTest
@MethodSource("argsProviderFactory")
void testWithMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> argsProviderFactory() {
    return Stream.of("alex", "brian");
}

If we do not explicitly provide a factory method name via @MethodSource, JUnit will search for a factory method that has the same name as the current @ParameterizedTest method by default.

So in the given example, if we do not provide the method name argsProviderFactory in @MethodSource annotation, Junit will search for a method name testWithMethodSource with return type Stream<String>.

@ParameterizedTest
@MethodSource
void testWithMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> testWithMethodSource() {
    return Stream.of("alex", "brian");
}

Streams for primitive types (DoubleStream, IntStream, and LongStream) are also supported.

@ParameterizedTest
@MethodSource("argsProviderFactory")
void testWithMethodSource(int argument) {
    assertNotEquals(9, argument);
}

static IntStream argsProviderFactory() {
    return IntStream.range(0, 10);
}

3.7. @CsvSource

It allows us to express argument lists as comma-separated values. Each CSV token represents a CSV line and results in one invocation of the parameterized test.

Set property ignoreLeadingAndTrailingWhitespace either true or false, which hints the Junit to honor or dishonor the whitespaces in CSV tokens.

@ParameterizedTest
@CsvSource(value = {
    "alex, 30",
    "brian, 35",
    "charles, 40"
}, ignoreLeadingAndTrailingWhitespace = true)
void testWithCsvSource(String name, int age) {
    assertNotNull(name);
    assertTrue(age > 0);
}

3.8. @CsvFileSource

It is very mi=uch similar to @CsvSource except we are reading the CSV tokens from a file instead of reading inline tokens. The CSV file may be read from the classpath or the local file system.

The default delimiter is a comma (,), but we can use another character by setting the delimiter attribute.

Note that any line beginning with a # symbol will be interpreted as a comment and will be ignored.

@ParameterizedTest
@CsvFileSource(resources = "employeeData.csv", numLinesToSkip = 0)
void testWithCsvFileSource(String name, int age) {
	assertNotNull(name);
    assertTrue(age > 0);
}

3.9. @ArgumentsSource

@ArgumentsSource can be used to specify a custom, reusable ArgumentsProvider.

@ParameterizedTest(name = "{index} - {0} is older than 40")
@ArgumentsSource(EmployeesArgumentsProvider.class)
void isEmployeeAgeGreaterThan40(Employee e) {
    assertTrue(Period.between(e.getDob(), LocalDate.now()).get(ChronoUnit.YEARS) > 40);
}

class EmployeesArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
          Arguments.of(new Employee(1, "Alex", LocalDate.of(1980, 2, 3))),
          Arguments.of(new Employee(2, "Brian", LocalDate.of(1979, 2, 3))),
          Arguments.of(new Employee(3, "Charles", LocalDate.of(1978, 2, 3)))
        );
    }
}

4. Parameterized tests multiple arguments

To write tests that can consume multiple arguments, we can use the following annotations:

4.1. @CsvSource

As shown in previous section 3.7, we can provide many literals and simple types of arguments using the @CsvSource annotation.

We need to provide all arguments in a single CSV token and then define the matching method arguments.

@ParameterizedTest
@CsvSource({
    "alex, 30, HR, Active",
    "brian, 35, Technology, Active",
    "charles, 40, Finance, Purged"
})
void testWithCsvSource(String name, int age, String department, String status) {
	//test code
}

4.2. ArgumentsProvider inteface

To provide multiple test arguments of complex or custom types, we should be using @ArgumentsSource annotation with ArgumentsProvider annotation.

In the given example, we are passing three arguments to the test method testArgumentsSource, of types Employee, LocalDate and enum constant of type Direction.

@ParameterizedTest
@ArgumentsSource(EmployeesArgumentsProvider.class)
void testArgumentsSource(Employee e, LocalDate date, Direction d) {
    assertTrue(Period.between(e.getDob(), LocalDate.now()).get(ChronoUnit.YEARS) > 40);
    assertNotNull(date);
    assertNotNull(d);
}

class EmployeesArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
          Arguments.of(new Employee(1, "Alex", 
        		  LocalDate.of(1980, 2, 3)), LocalDate.now(), Direction.EAST),
          Arguments.of(new Employee(2, "Brian", 
        		  LocalDate.of(1979, 2, 3)), LocalDate.now(), Direction.NORTH),
          Arguments.of(new Employee(3, "Charles", 
        		  LocalDate.of(1978, 2, 3)), LocalDate.now(), Direction.SOUTH)
        );
    }

5. Conclusion

Junit 5 @ParameterizedTest annotation is very helpful in writing tests that must be invoked multiple times but with different arguments to test. Junit provides multiple ways which can be used declaratively to provide arguments to the test method.

We can combine different annotations to test various kinds of inputs in a single test as well.

There are some more complex concepts such as arguments aggregator, arguments converter etc. Please read the official Junit documentation for the latest information.

Happy Learning !!

Source Code on Github

Was this post helpful?

Join 7000+ Fellow Programmers

Subscribe to get new post notifications, industry updates, best practices, and much more. Directly into your inbox, for free.

Leave a Comment

HowToDoInJava

A blog about Java and its related technologies, the best practices, algorithms, interview questions, scripting languages, and Python.