This blog post describes how we can write parameterized tests with JUnit 5. After we have finished this blog post, we:
- Can get the required dependencies with Maven and Gradle.
- Know how we can customize the display name of each method invocation.
- Understand how we can use different argument sources.
- Can write custom argument converters.
Let’s start by getting the required dependencies.
This blog post assumes that:
Getting the Required Dependencies
Before we can write parameterized tests with JUnit 5, we have to ensure that the junit-jupiter-params dependency is found from the classpath. If we are using the junit-jupiter aggregator artifact, we don’t have to do anything because all the required dependencies are already added to the classpath. On the other hand, if we aren’t using the junit-jupiter aggregator artifact, we have to make some changes to our build script.
If we are using Maven, we have to add the junit-jupiter-params dependency to the test scope. We can do this by adding the following snippet to dependencies section of our POM file:
1<dependency>
2 <groupId>org.junit.jupiter</groupId>
3 <artifactId>junit-jupiter-params</artifactId>
4 <version>5.8.2</version>
5 <scope>test</scope>
6</dependency>
If we are using Gradle, we have to add the junit-jupiter-params dependency to the testImplementation dependency configuration. We can do this by adding the following snippet to our build.gradle file:
1testImplementation(
2 'org.junit.jupiter:junit-jupiter-params:5.8.2'
3)
Let’s move on and write our first parameterized test with JUnit 5.
Writing Our First Parameterized Tests
If our test method takes only one method parameter that’s either a String or a primitive type supported by the @ValueSource annotation (byte, char, double, float, int, long, or short), we can write a parameterized test with JUnit 5 by following these steps:
- Add a new test method to our test class and ensure that this method takes a
Stringobject as a method parameter. - Configure the display name of the test method.
- Annotate the test method with the
@ParameterizedTestannotation. This annotation identifies parameterized test methods. - Provide the method parameters which are passed to our test method. Because our test method takes one
Stringobject as a method parameter, we can provide its method parameters by annotating our test method with the@ValueSourceannotation.
After we have added a new parameterized test to our test class, its source code looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.provider.ValueSource;
4
5import static org.junit.jupiter.api.Assertions.assertNotNull;
6
7@DisplayName("Pass the method parameters provided by the @ValueSource annotation")
8class ValueSourceExampleTest {
9
10 @DisplayName("Should pass a non-null message to our test method")
11 @ParameterizedTest
12 @ValueSource(strings = {"Hello", "World"})
13 void shouldPassNonNullMessageAsMethodParameter(String message) {
14 assertNotNull(message);
15 }
16}
Additional Reading:
When we run our parameterized test, we should see an output that looks as follows:
Pass the method parameters provided by the @ValueSource annotation
|_ Should pass a non-null message to our test method
|_ [1] Hello
|_ [2] World
Even though this output looks quite clean, sometimes we want to provide our own display name for each method invocation. Let’s find out how we can do it.
Customizing the Display Name of Each Method Invocation
We can customize the display name of each method invocation by setting the value of the @ParameterizedTest annotation’s name attribute. This attribute supports the following placeholders:
{displayName}: The display name of the test method.{index}: The index of the current invocation. Note that the index of the first invocation is one.{arguments}: A comma separated list that contains all arguments passed to the test method.{argumentsWithNames}: A comma separated list that contains all arguments (including the name of the method parameter) passed to the test method.{i}: The actual method parameter (ispecifies the index of the method parameter). Note that the index of the first method parameter is zero.
Let’s provide a custom display name to our test method. This display name must display the index of the current invocation and the provided method parameter. After we have configured the custom display name of each method invocation, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.provider.ValueSource;
4
5import static org.junit.jupiter.api.Assertions.assertNotNull;
6
7@DisplayName("Pass the method parameters provided by the @ValueSource annotation")
8class ValueSourceExampleTest {
9
10 @DisplayName("Should pass a non-null message to our test method")
11 @ParameterizedTest(name = "{index} => message=''{0}''")
12 @ValueSource(strings = {"Hello", "World"})
13 void shouldPassNonNullMessageAsMethodParameter(String message) {
14 assertNotNull(message);
15 }
16}
When we run our parameterized test, we should see an output that looks as follows:
Pass the method parameters provided by the @ValueSource annotation
|_ Should pass a non-null message to our test method
|_ 1 => message='Hello'
|_ 2 => message='World'
Additional Reading:
As we remember, the @ValueSource annotation is a good choice if our test method takes only one method parameter that’s supported by the @ValueSource annotation. However, most of the time this is not the case. Next, we will find out how we can solve this problem by using different argument sources.
Using Argument Sources
The @ValueSource annotation is the simplest argument source that’s supported by JUnit 5. However, JUnit 5 support other argument sources as well. All supported argument sources are configured by using annotations found from the org.junit.jupiter.params.provider package.
This section describes how we can use the more complex argument sources provided by JUnit 5. Let’s start by finding out how we can pass enum values to our parameterized test.
Passing Enum Values to Our Parameterized Test
If our parameterized test takes one enum value as a method parameter, we have to annotate our test method with the @EnumSource annotation and specify the enum values which are passed to our test method.
Let’s assume that we have to write a parameterized test that takes a value of the Pet enum as a method parameter. The source code of the Pet enum looks as follows:
1enum Pet {
2 CAT,
3 DOG;
4}
If we want to pass all enum values to our test method, we have to annotate our test method with the @EnumSource annotation and specify the enum whose values are passed to our test method. After we have done this, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.provider.EnumSource;
4
5import static org.junit.jupiter.api.Assertions.assertNotNull;
6
7@DisplayName("Pass enum values to our test method")
8class EnumSourceExampleTest {
9
10 @DisplayName("Should pass non-null enum values as method parameters")
11 @ParameterizedTest(name = "{index} => pet=''{0}''")
12 @EnumSource(Pet.class)
13 void shouldPassNonNullEnumValuesAsMethodParameter(Pet pet) {
14 assertNotNull(pet);
15 }
16}
When we run this test method, we see that JUnit 5 passes all values of the Pet enum to our test method:
Pass enum values to our test method
|_ Should pass non-null enum values as method parameters
|_ 1 => pet='CAT'
|_ 2 => pet='DOG'
If we want to specify the enum values that are passed to our test method, we can specify the enum values by setting the value of the @EnumSource annotation’s names attribute. Let’s ensure that the value: Pet.CAT is passed to our test method.
After we have specified the used enum value, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.provider.EnumSource;
4
5import static org.junit.jupiter.api.Assertions.assertNotNull;
6
7@DisplayName("Pass enum values to our test method")
8class EnumSourceExampleTest {
9
10 @DisplayName("Should pass only the specified enum value as a method parameter")
11 @ParameterizedTest(name = "{index} => pet=''{0}''")
12 @EnumSource(value = Pet.class, names = {"CAT"})
13 void shouldPassNonNullEnumValueAsMethodParameter(Pet pet) {
14 assertNotNull(pet);
15 }
16}
When we run this test method, we see that JUnit 5 passes only the value: Pet.CAT to our test method:
Pass enum values to our test method
|_ Should pass non-null enum values as method parameters
|_ 1 => pet='CAT'
The @EnumSource annotation provides quite versatile support for filtering enum values. If you want to get more information about this, you should take a look at the following documents:
Additional Reading:
We have now learned how we can use two different argument sources that allow us to pass one method parameter to our test method. However, most of the time we want to pass multiple method parameters to our parameterized test. Next, we will find out how we can solve this problem by using the CSV format.
Creating Our Test Data by Using the CSV Format
If we have to pass multiple arguments to the invoked test method and the provided test data is used by only one test method (or a few test methods), we can configure our test data by using the @CsvSource annotation. When we add this annotation to a test method, we have to configure the test data by using an array of String objects. When we specify our test data, we have to follow these rules:
- One
Stringobject must contain all arguments of one method invocation. - The different argument values must be separated with a comma.
- The argument values found from each line must use the same order as the method parameters of our test method.
Let’s configure the arguments which are passed to the sum() method. This method takes three method parameters: the first two method parameters contain two int values and the third method parameter specifies the expected sum of the provided int values.
After we have configured the test data of our parameterized test, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.provider.CsvSource;
4
5import static org.junit.jupiter.api.Assertions.assertEquals;
6
7@DisplayName("Should pass the method parameters provided by the @CsvSource annotation")
8class CsvSourceExampleTest {
9
10 @DisplayName("Should calculate the correct sum")
11 @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
12 @CsvSource({
13 "1, 1, 2",
14 "2, 3, 5"
15 })
16 void sum(int a, int b, int sum) {
17 assertEquals(sum, a + b);
18 }
19}
Additional Reading:
Even though this looks quite clean, sometimes we have so much test data that it doesn’t make sense to add it to our test class because our test class would become unreadable. Let’s find out how we can load the test data that’s passed to the sum() method from a CSV file.
Loading Our Test Data From a CSV File
We can load our test data from a CSV file by following these steps:
First, we have to create a CSV file that contains our test data and put this file to the classpath. When we add our test data to the created CSV file, we have to follow these rules:
- One line must contain all arguments of one method invocation.
- The different argument values must be separated with a comma.
- The argument values found from each line must use the same order as the method parameters of our test method.
The test-data.csv file configures the test data that is passed to the sum() method. This file is found from the src/test/resources directory, and its content looks as follows:
1,1,2
2,3,5
3,5,8
Second, we have to annotate our test method with the @CsvFileSource annotation and configure the location of our CSV file. After we have done this, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.provider.CsvFileSource;
4
5import static org.junit.jupiter.api.Assertions.assertEquals;
6
7@DisplayName("Should pass the method parameters provided by the test-data.csv file")
8class CsvFileSourceExampleTest {
9
10 @DisplayName("Should calculate the correct sum")
11 @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
12 @CsvFileSource(resources = "/test-data.csv")
13 void sum(int a, int b, int sum) {
14 assertEquals(sum, a + b);
15 }
16}
Additional Reading:
We can now pass multiple method parameters to our parameterized test. However, the catch is that the method parameters of our parameterized tests must be supported by the DefaultArgumentConverter class. Its Javadoc states that:
The DefaultArgumentConverter is able to convert from strings to a number of primitive types and their corresponding wrapper types (Byte, Short, Integer, Long, Float, and Double), date and time types from the java.time package, and some additional common Java types such as File, BigDecimal, BigInteger, Currency, Locale, URI, URL, UUID, etc.
Next, we will find out how we can solve this problem by using a factory method and a custom ArgumentsProvider.
Creating Our Test Data by Using a Factory Method
If all parameterized tests which use the created test data are found from the same test class and the logic that creates the test data isn’t “too complex”, we should create our test data by using a factory method.
If we want to use this approach, we have to add a static factory method to our test class and implement this method by following these rules:
- The factory method must not take any method parameters.
- The factory method must return a
Stream,Iterable,Iterator, or an array ofArgumentsobjects. The object returned by our factory method contains the arguments of all test method invocations. - An
Argumentsobject must contain all arguments of a single test method invocation. - We can create a new
Argumentsobject by invoking thestatic of()method of theArgumentsinterface. The arguments provided to theof()method are passed to our test method when it’s invoked by JUnit 5. That’s why the provided arguments must use the same order as the method parameters of our test method.
There are two things I want to point out:
- The factory method must be
staticonly if we use the default lifecycle configuration. - The factory method can also return a
Streamthat contain primitive types. This blog post doesn’t use this approach because I wanted to demonstrate how we can pass “complex” objects to our parameterized tests.
Let’s demonstrate these rules by implementing a factory method that creates the test data which is passed to the sum() method (we have already used this method in the previous examples). After we have implemented this factory method, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.provider.Arguments;
4
5import java.util.stream.Stream;
6
7import static org.junit.jupiter.api.Assertions.assertEquals;
8
9@DisplayName("Should pass the method parameters provided by the sumProvider() method")
10class MethodSourceExampleTest {
11
12 @DisplayName("Should calculate the correct sum")
13 @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
14 void sum(int a, int b, int sum) {
15 assertEquals(sum, a + b);
16 }
17
18 private static Stream<Arguments> sumProvider() {
19 return Stream.of(
20 Arguments.of(1, 1, 2),
21 Arguments.of(2, 3, 5)
22 );
23 }
24}
After we have implemented this method, we must ensure that its return value is used when JUnit 5 runs our parameterized test method. We can do this by following these steps:
- Annotate our test method with the
@MethodSourceannotation. - Configure the name of the factory method which creates our test data.
After we have made the required changes to our test class, its source code looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.provider.Arguments;
4import org.junit.jupiter.params.provider.MethodSource;
5
6import java.util.stream.Stream;
7
8import static org.junit.jupiter.api.Assertions.assertEquals;
9
10@DisplayName("Should pass the method parameters provided by the sumProvider() method")
11class MethodSourceExampleTest {
12
13 @DisplayName("Should calculate the correct sum")
14 @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
15 @MethodSource("sumProvider")
16 void sum(int a, int b, int sum) {
17 assertEquals(sum, a + b);
18 }
19
20 private static Stream<Arguments> sumProvider() {
21 return Stream.of(
22 Arguments.of(1, 1, 2),
23 Arguments.of(2, 3, 5)
24 );
25 }
26}
Additional Reading:
This approach works relatively well as long as the factory method is simple and all test methods which use the factory method are found from the same test class. If either of these conditions is false, we have to implement a custom ArgumentsProvider.
Creating Our Test Data by Using a Custom ArgumentsProvider
If the test methods which use our test data are found from different test classes or the logic which creates the required test data is so complex that we don’t want to add it to our test class, we have to create a custom ArgumentsProvider.
We can do this by creating class that implements the ArgumentsProvider interface. After we have created this class, we have to implement the provideArguments() method that returns a Stream of Arguments objects. When we create the returned Stream object, we must follow these rules:
- The returned object must contain the arguments of all test method invocations.
- An
Argumentsobject must contain all arguments of a single test method invocation. - We can create a new
Argumentsobject by invoking thestatic of()method of theArgumentsinterface. The arguments provided to theof()method are passed to our test method when it’s invoked by JUnit 5. That’s why the provided arguments must use the same order as the method parameters of our test method.
Let’s create a custom ArgumentsProvider which provides the test data that’s passed to the sum() method. We can do this by following these steps:
First, we have write a custom ArgumentsProvider class which returns the test data that’s passed to the sum() method.
After we have created a custom ArgumentsProvider class, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.api.extension.ExtensionContext;
3import org.junit.jupiter.params.ParameterizedTest;
4import org.junit.jupiter.params.provider.Arguments;
5import org.junit.jupiter.params.provider.ArgumentsProvider;
6
7import java.util.stream.Stream;
8
9import static org.junit.jupiter.api.Assertions.assertEquals;
10
11@DisplayName("Should pass the method parameters provided by the CustomArgumentProvider class")
12class ArgumentsSourceExampleTest {
13
14 @DisplayName("Should calculate the correct sum")
15 @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
16 void sum(int a, int b, int sum) {
17 assertEquals(sum, a + b);
18 }
19
20 static class CustomArgumentProvider implements ArgumentsProvider {
21
22 @Override
23 public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
24 return Stream.of(
25 Arguments.of(1, 1, 2),
26 Arguments.of(2, 3, 5)
27 );
28 }
29 }
30}
Second, we have to configure the used ArgumentsProvider by annotating our test method with the @ArgumentsSource annotation. After we have done this, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.api.extension.ExtensionContext;
3import org.junit.jupiter.params.ParameterizedTest;
4import org.junit.jupiter.params.provider.Arguments;
5import org.junit.jupiter.params.provider.ArgumentsProvider;
6import org.junit.jupiter.params.provider.ArgumentsSource;
7
8import java.util.stream.Stream;
9
10import static org.junit.jupiter.api.Assertions.assertEquals;
11
12@DisplayName("Should pass the method parameters provided by the CustomArgumentProvider class")
13class ArgumentsSourceExampleTest {
14
15 @DisplayName("Should calculate the correct sum")
16 @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
17 @ArgumentsSource(CustomArgumentProvider.class)
18 void sum(int a, int b, int sum) {
19 assertEquals(sum, a + b);
20 }
21
22 static class CustomArgumentProvider implements ArgumentsProvider {
23
24 @Override
25 public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
26 return Stream.of(
27 Arguments.of(1, 1, 2),
28 Arguments.of(2, 3, 5)
29 );
30 }
31 }
32}
Additional Reading:
We can now create our test data by using factory methods and custom ArgumentsProvider classes. However, even though these methods allow us to ignore the limitations of the DefaultArgumentConverter class, sometimes we want to provide our test data by using strings because this helps us to write tests which are easier to read than tests which use factory methods or custom ArgumentsProvider classes.
Next, we will find out how we can solve this problem by using a custom ArgumentConverter.
Using a Custom ArgumentConverter
An ArgumentConverter has only one responsibility: it converts the source object into an instance of another type. If the conversion fails, it must throw an ArgumentConversionException.
Let’s create an ArgumentConverter that can convert a String object into a Message object. The Message class is a simple wrapper class that simply wraps the message given as a constructor argument. Its source code looks as follows:
1final class Message {
2
3 private final String message;
4
5 Message(String message) {
6 this.message = message;
7 }
8
9 String getMessage() {
10 return message;
11 }
12}
We can create our custom ArgumentConverter by following these steps:
First, we have to create a class called MessageConverter that implements the ArgumentConverter interface. After we have created this class, its source code looks as follows:
1import org.junit.jupiter.api.extension.ParameterContext;
2import org.junit.jupiter.params.converter.ArgumentConversionException;
3import org.junit.jupiter.params.converter.ArgumentConverter;
4
5final class MessageConverter implements ArgumentConverter {
6
7 @Override
8 public Object convert(Object source, ParameterContext context) throws ArgumentConversionException {
9
10 }
11}
Second, we have to implement the convert() method by following these steps:
- Throw a new
ArgumentConversionExceptionif the source object isn’t valid. The source object must be aStringthat’s notnullor empty. - Create a new
Messageobject and return the created object.
After we have implemented the convert() method, the source code of the MessageConverter class looks as follows:
1import org.junit.jupiter.api.extension.ParameterContext;
2import org.junit.jupiter.params.converter.ArgumentConversionException;
3import org.junit.jupiter.params.converter.ArgumentConverter;
4
5final class MessageConverter implements ArgumentConverter {
6
7 @Override
8 public Object convert(Object source, ParameterContext context) throws ArgumentConversionException {
9 checkSource(source);
10
11 String sourceString = (String) source;
12 return new Message(sourceString);
13 }
14
15 private void checkSource(Object source) {
16 if (source == null) {
17 throw new ArgumentConversionException("Cannot convert null source object");
18 }
19
20 if (!source.getClass().equals(String.class)) {
21 throw new ArgumentConversionException(
22 "Cannot convert source object because it's not a string"
23 );
24 }
25
26 String sourceString = (String) source;
27 if (sourceString.trim().isEmpty()) {
28 throw new ArgumentConversionException(
29 "Cannot convert an empty source string"
30 );
31 }
32 }
33}
After we have created our custom ArgumentConverter, we have to create a parameterized test which uses our custom ArgumentConverter. We can create this test by following these steps:
First, we have to create a new parameterized test method by following these steps:
- Add a new parameterized test method to our test class and ensure that the method takes two
Messageobjects as method parameters. - Annotate the test method with the
@CsvSourceannotation and configure the test data by using the CSV format. - Verify that the
Messageobjects given as method parameters contain the same message.
After we have created our test method, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.provider.CsvSource;
4
5import static org.junit.jupiter.api.Assertions.assertEquals;
6
7@DisplayName("Pass converted Message objects to our test method")
8class MessageConverterExampleTest {
9
10 @DisplayName("Should pass same messages as method parameters")
11 @ParameterizedTest(name = "{index} => actual={0}, expected={1}")
12 @CsvSource({
13 "Hello, Hello",
14 "Hi, Hi",
15 })
16 void shouldPassMessages(Message actual, Message expected) {
17 assertEquals(expected.getMessage(), actual.getMessage());
18 }
19}
Second, we have to configure the ArgumentConverter that creates the arguments passed to our test method. We can do this by annotating the method parameters with the @ConvertWith annotation. When we do this, we have to configure the used ArgumentConverter by setting the value of the @ConvertWith annotation’s value attribute.
After we have configured the used ArgumentConverter, the source code of our test class looks as follows:
1import org.junit.jupiter.api.DisplayName;
2import org.junit.jupiter.params.ParameterizedTest;
3import org.junit.jupiter.params.converter.ConvertWith;
4import org.junit.jupiter.params.provider.CsvSource;
5
6import static org.junit.jupiter.api.Assertions.assertEquals;
7
8@DisplayName("Pass converted Message objects to our test method")
9class MessageConverterExampleTest {
10
11 @DisplayName("Should pass same messages as method parameters")
12 @ParameterizedTest(name = "{index} => actual={0}, expected={1}")
13 @CsvSource({
14 "Hello, Hello",
15 "Hi, Hi",
16 })
17 void shouldPassMessages(@ConvertWith(MessageConverter.class) Message actual,
18 @ConvertWith(MessageConverter.class) Message expected) {
19 assertEquals(expected.getMessage(), actual.getMessage());
20 }
21}
If you want to get access to up-to-date material which is essentially a better version of my JUnit 5 tutorial, you should take a look at my Introduction to JUnit 5 course.
Additional Reading:
We can now write parameterized tests with JUnit 5. Let’s summarize what we learned from this blog post.
Summary
This blog post has taught us seven things:
- Before we can write parameterized tests with JUnit 5, we have to ensure that the
junit-jupiter-paramsdependency is found from the classpath. - We have to annotate our parameterized test method with the
@ParameterizedTestannotation. - We can customize the display name of each method invocation by setting the value of the
@ParameterizedTestannotation’snameattribute. - When we configure our test data, our test data must use the same order as the method parameters of our test method.
- If we want pass “complex” objects to parameterized tests which are found from the same test class and the logic that creates these arguments isn’t “too complex”, we should create these arguments by using a factory method.
- If the test methods which use our test data are found from different test classes or the logic which creates the required test data is so complex that we don’t want to add it to our test class, we have to create a custom
ArgumentsProvider. - If we want to provide our test data by using strings and use method parameters which aren’t supported by the default argument converters, we have to implement a custom
ArgumentConverter.
P.S. You can get the example application of this blog post from Github.