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 parameterword
. - 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 !!