Learn to write automated tests for Spring and Spring Boot Web Apps: Test With Spring course

JUnit 5 Tutorial: Writing Parameterized Tests

This blog post describes how we can write parameterized tests with JUnit 5. To be more specific, we will learn to: get the required dependencies, customize the display name of each method invocation, use different argument sources, and 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 declare the junit-jupiter-params dependency in our build script.

If we are using Maven, we have to add the following snippet to our POM file:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.0.2</version>
    <scope>test</scope>
</dependency>

If we are using Gradle, we have to add the junit-jupiter-params dependency to the testCompile dependency configuration. We can do this by adding the following snippet to our build.gradle file:

	
testCompile(
        'org.junit.jupiter:junit-jupiter-params:5.0.2'
)

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 is either a String or a primitive type supported by the @ValueSource annotation (int, long, or double), we can write a parameterized test with JUnit 5 by following these steps:

  1. Add a new test method to our test class and ensure that this method takes a String object as a method parameter.
  2. Configure the display name of the test method.
  3. Annotate the test method with the @ParameterizedTest annotation. This annotation identifies parameterized test methods.
  4. Provide the method parameters that are passed to our test method. Because our test method takes one String object as a method parameter, we can provide its method parameters by annotating our test method with the @ValueSource annotation.

After we have added a new parameterized test to our test class, its source code looks as follows:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@DisplayName("Pass the method parameters provided by the @ValueSource annotation")
class ValueSourceExampleTest {

    @DisplayName("Should pass a non-null message to our test method")
    @ParameterizedTest
    @ValueSource(strings = {"Hello", "World"})
    void shouldPassNonNullMessageAsMethodParameter(String message) {
        assertNotNull(message);
    }
}

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:

  • {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 method parameters.
  • {i}: The actual method parameter (i specifies 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:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@DisplayName("Pass the method parameters provided by the @ValueSource annotation")
class ValueSourceExampleTest {

    @DisplayName("Should pass a non-null message to our test method")
    @ParameterizedTest(name = "{index} => message=''{0}''")
    @ValueSource(strings = {"Hello", "World"})
    void shouldPassNonNullMessageAsMethodParameter(String message) {
        assertNotNull(message);
    }
}

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'

As we remember, the @ValueSource annotation is a good choice if our test method takes only one method parameter that is 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 is 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:

enum Pet {
    CAT,
    DOG;
}

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:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@DisplayName("Pass enum values to our test method")
class EnumSourceExampleTest {

    @DisplayName("Should pass non-null enum values as method parameters")
    @ParameterizedTest(name = "{index} => pet=''{0}''")
    @EnumSource(Pet.class)
    void shouldPassNonNullEnumValuesAsMethodParameter(Pet pet) {
        assertNotNull(pet);
    }
}

When we run this test method, we see that JUnit 5 passes all values of the Pet enum to our test method as method parameters:

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:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@DisplayName("Pass enum values to our test method")
class EnumSourceExampleTest {

    @DisplayName("Should pass only the specified enum value as a method parameter")
    @ParameterizedTest(name = "{index} => pet=''{0}''")
    @EnumSource(value = Pet.class, names = {"CAT"})
    void shouldPassNonNullEnumValueAsMethodParameter(Pet pet) {
        assertNotNull(pet);
    }
}

When we run this test method, we see that JUnit 5 passes only the value: Pet.CAT to our test method as a method parameter:

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 resources:

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.

Passing Method Parameters by Using the CSV Format

If we have to pass multiple method parameters to our parameterized test 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 String object must contain all method parameters of one method invocation.
  • The different method parameters must be separated with a comma.
  • The values found from each line must use the same order as the method parameters of our test method.

Let’s configure the method parameters that 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 method parameters of our parameterized test, the source code of our test class looks as follows:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

@DisplayName("Should pass the method parameters provided by the @CsvSource annotation")
class CsvSourceExampleTest {

    @DisplayName("Should calculate the correct sum")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
    @CsvSource({
            "1, 1, 2",
            "2, 3, 5"
    })
    void sum(int a, int b, int sum) {
        assertEquals(sum, a + b);
    }
}

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 is 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 method parameters of one method invocation.
  • The different method parameters must be separated with a comma.
  • The 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 our test 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:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

@DisplayName("Should pass the method parameters provided by the test-data.csv file")
class CsvFileSourceExampleTest {

    @DisplayName("Should calculate the correct sum")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
    @CsvFileSource(resources = "/test-data.csv")
    void sum(int a, int b, int sum) {
        assertEquals(sum, a + b);
    }
}

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) as well as date and time types from the java.time package.

Next, we will find out how we can solve this problem by using a factory method and a Custom ArgumentsProvider.

Creating the Method Parameters by Using a Factory Method

If all parameterized tests which use the created method parameters are found from the same test class and the logic that creates these method parameters is not “too complex”, we should create these method parameters 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 of Arguments objects. The object returned by our factory method contains the method parameters of all test method invocations.
  • An Arguments object must contain all method parameters of a single test method invocation.
  • We can create a new Arguments object by invoking the static of() method of the Arguments interface. The method parameters provided to the of() method are passed to our test method when it is invoked by JUnit 5. That’s why the provided method parameters 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 static only if we use the default lifecycle configuration.
  • The factory method can also return a Stream that 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 method parameters of 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:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

@DisplayName("Should pass the method parameters provided by the sumProvider() method")
class MethodSourceExampleTest {

    @DisplayName("Should calculate the correct sum")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
    void sum(int a, int b, int sum) {
        assertEquals(sum, a + b);
    }

    private static Stream<Arguments> sumProvider() {
        return Stream.of(
                Arguments.of(1, 1, 2),
                Arguments.of(2, 3, 5)
        );
    }
}

After we have implemented this method, we must ensure that its return value is used when JUnit 5 determines the method parameters of our parameterized test. We can do this by annotating our test method with the @MethodSource annotation. When we do this, we must remember to configure the name of the factory method.

After we have made the required changes to our test class, its source code looks as follows:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

@DisplayName("Should pass the method parameters provided by the sumProvider() method")
class MethodSourceExampleTest {

    @DisplayName("Should calculate the correct sum")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
    @MethodSource("sumProvider")
    void sum(int a, int b, int sum) {
        assertEquals(sum, a + b);
    }

    private static Stream<Arguments> sumProvider() {
        return Stream.of(
                Arguments.of(1, 1, 2),
                Arguments.of(2, 3, 5)
        );
    }
}

This approach work relatively well as long as the factory method is simple and all test methods that 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 the Method Parameters by Using a Custom ArgumentsProvider

If the test methods that 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, we must follow these rules:

  • An Arguments object must contain all method parameters of a single test method invocation.
  • We can create a new Arguments object by invoking the static of() method of the Arguments interface. The method parameters provided to the of() method are passed to our test method when it is invoked by JUnit 5. That’s why the provided method parameters 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 is required by 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 is required by the sum() method.

After we have created a custom ArgumentsProvider class, the source code of our test class looks as follows:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

@DisplayName("Should pass the method parameters provided by the CustomArgumentProvider class")
class ArgumentsSourceExampleTest {

    @DisplayName("Should calculate the correct sum")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
    void sum(int a, int b, int sum) {
        assertEquals(sum, a + b);
    }

    static class CustomArgumentProvider implements ArgumentsProvider {

        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
            return Stream.of(
                    Arguments.of(1, 1, 2),
                    Arguments.of(2, 3, 5)
            );
        }
    }
}
By the way, most of the time we don’t want to use inner classes. I used an inner class here because this is just an example.

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 test class looks as follows:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

@DisplayName("Should pass the method parameters provided by the CustomArgumentProvider class")
class ArgumentsSourceExampleTest {

    @DisplayName("Should calculate the correct sum")
    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
    @ArgumentsSource(CustomArgumentProvider.class)
    void sum(int a, int b, int sum) {
        assertEquals(sum, a + b);
    }

    static class CustomArgumentProvider implements ArgumentsProvider {

        @Override
        public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
            return Stream.of(
                    Arguments.of(1, 1, 2),
                    Arguments.of(2, 3, 5)
            );
        }
    }
}

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 that 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 should 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:

final class Message {

    private final String message;

    Message(String message) {
        this.message = message;
    }

    String getMessage() {
        return message;
    }
}

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:

import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.params.converter.ArgumentConversionException;
import org.junit.jupiter.params.converter.ArgumentConverter;

final class MessageConverter implements ArgumentConverter {

    @Override
    public Object convert(Object source, ParameterContext context) throws ArgumentConversionException {
        
    }
}

Second, we have to implement the convert() method by following these steps:

  1. Throw a new ArgumentConversionException if the source object is not valid. The source object must be a String that is not null or empty.
  2. Create a new Message object and return the created object.

After we have implemented the convert() method, the source code of the MessageConverter class looks as follows:

import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.params.converter.ArgumentConversionException;
import org.junit.jupiter.params.converter.ArgumentConverter;

final class MessageConverter implements ArgumentConverter {

    @Override
    public Object convert(Object source, ParameterContext context) throws ArgumentConversionException {
        checkSource(source);

        String sourceString = (String) source;
        return new Message(sourceString);
    }

    private void checkSource(Object source) {
        if (source == null) {
            throw new ArgumentConversionException("Cannot convert null source object");
        }

        if (!source.getClass().equals(String.class)) {
            throw new ArgumentConversionException(
                    "Cannot convert source object because it's not a string"
            );
        }

        String sourceString = (String) source;
        if (sourceString.trim().isEmpty()) {
            throw new ArgumentConversionException(
                    "Cannot convert an empty source string"
            );
        }
    }
}

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:

  1. Add a new parameterized test method to our test class and ensure that the method takes two Message objects as method parameters.
  2. Annotate the test method with the @CsvSource annotation and configure the test data by using the CSV format.
  3. Verify that the Message objects 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:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

@DisplayName("Pass converted Message objects to our test method")
class MessageConverterExampleTest {

    @DisplayName("Should pass same messages as method parameters")
    @ParameterizedTest(name = "{index} => actual={0}, expected={1}")
    @CsvSource({
            "Hello, Hello",
            "Hi, Hi",
    })
    void shouldPassMessages(Message actual, Message expected) {
        assertEquals(expected.getMessage(), actual.getMessage());
    }
}

Second, we have to configure the ArgumentConverter that creates the actual method parameters. 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:

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.ConvertWith;
import org.junit.jupiter.params.provider.CsvSource;

import static org.junit.jupiter.api.Assertions.assertEquals;

@DisplayName("Pass converted Message objects to our test method")
class MessageConverterExampleTest {

    @DisplayName("Should pass same messages as method parameters")
    @ParameterizedTest(name = "{index} => actual={0}, expected={1}")
    @CsvSource({
            "Hello, Hello",
            "Hi, Hi",
    })
    void shouldPassMessages(@ConvertWith(MessageConverter.class) Message actual,
                            @ConvertWith(MessageConverter.class) Message expected) {
        assertEquals(expected.getMessage(), actual.getMessage());
    }
}

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 declare the junit-jupiter-params dependency in our build script.
  • We have to annotate our parameterized test method with the @ParameterizedTest annotation.
  • We can customize the display name of each method invocation by setting the value of the @ParameterizedTest annotation’s name attribute.
  • 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 method parameters is not “too complex”, we should create these method parameters by using a factory method.
  • If the test methods that 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 that are not 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.

About the Author

Petri Kainulainen is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.

About Petri Kainulainen →

3 comments… add one
  • Hi Petri

    Thanks by this valuable tutorial.

    I have the following in JUnit 4, what is your best recommendation about if is necessary to adapt it to JUnit 5 (perhaps is not necessary)

    @Parameters(name="{index}")
    public static Collection data() throws URISyntaxException {
    	....
    }
    
    public SomeTestClass(Class1 a, ...,  ClassN n){
    	this.a = a;
        ...
        this.n = n;
    }
    

    From above some points:

    • The method is annotated with @Parameters
    • The data() method must have no parameters.
    • The class’ constructor must have the same set (1 … to N) of parameters and types (order is important) than the data returned in the data() method.

    Something interesting in your tutorial that perhaps should be highlighted, the are no @Test methods.

    Thanks

    -Manuel

    Reply
    • Hi Manuel,

      I think that migrating your existing tests to use JUnit 5 is not a good idea unless:

      • Modifying your existing tests doesn’t take a lot of time.
      • You don’t have too many tests and you have to change them anyway because you made changes to the system under test.
      • You have a problem that can solved only by using JUnit 5.

      The thing is that changing your existing code is basically an investment and you want to ensure that you get a good return on your investment. The “problem” is that it’s very hard to get even a decent ROI if your existing tests don’t have any problems AND the conditions I mentioned earlier are false.

      In other words, I guess the answer to your question is: it depends, but most of the time you shouldn’t do it.

      Reply

Leave a Comment