Writing Parameterized Tests With JUnit 5

If you want to learn how you can work smarter and save time when you are writing tests with JUnit 5, you should take a look at my Introduction to JUnit 5 course. It has 24 lessons, 47 exercises, and 13 quizzes.

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:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.8.2</version>
    <scope>test</scope>
</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:

testImplementation(
        'org.junit.jupiter:junit-jupiter-params:5.8.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'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:

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

  • {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 (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'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:

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:

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:

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 String object 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:

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

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), 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 of Arguments objects. The object returned by our factory method contains the arguments of all test method invocations.
  • An Arguments object must contain all arguments of a single test method invocation.
  • We can create a new Arguments object by invoking the static of() method of the Arguments interface. The arguments provided to the of() 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 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 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:

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 runs our parameterized test method. We can do this by following these steps:

  1. Annotate our test method with the @MethodSource annotation.
  2. 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:

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 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 Arguments object must contain all arguments of a single test method invocation.
  • We can create a new Arguments object by invoking the static of() method of the Arguments interface. The arguments provided to the of() 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:

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

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 isn't valid. The source object must be a String that's 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 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:

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());
    }
}
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-params dependency is found from the classpath.
  • 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 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.

17 comments… add one
  • Manuel Jordan Jan 1, 2018 @ 1:46

    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

    • Petri Jan 7, 2018 @ 20:53

      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.

  • rekha Oct 18, 2019 @ 23:05

    Hi Petri,
    Thank you for the documentation. Its quite elaborate.
    I am migrating all my tests from junit 4 to 5.
    I am writing a @ParameterizedTest with @MethodSource as input parameters provider.
    But the @MethodSource factory method I use has to get parameters from different methods as per the current tests that I have.

    Example:

    public String domain= registrationPage.getFullDomain();
    public String email= registrationPage.getEmail();
    public String password= registrationPage.getPassword();
    
     Stream registrationInputParameters() {
            return Stream.of(
                    Arguments.of(domain,  email, password)
            );
        }
    
        @ParameterizedTest()
        @MethodSource("registrationInputParameters")
        public void sampleTest(String domain, String email, String password) {
            RegistrationPage registrationPage= open(fullDomain);
    
            registrationPage.register(email, password);
    }
    

    These values are throwing nullpointer exceptions and as per your documentation about the @MethodSource factory method 'A factory method must not take any method parameters'. Can you please let me know if the above functionality can be achieved any way using any other argument sources provided by @ParameterizedTest in junit 5

    • Petri Oct 22, 2019 @ 18:52

      Hi,

      can you let me know what your use case is? It's clear that you want to pass domain, email address, and password to your parameterized test method, but I am not sure how do you specify your test data. For example, do you want to read it from an external source (such as a file or a database) or do you want use a "constant dataset" (a set of constant values)?

      • Rekha Oct 23, 2019 @ 0:48

        Thank you for replying.
        I want the test data from few base pages. Like I have a loginPage for all the login related data . In the above example I got the domain, email and password from the base registrationPage. This data from the loginPage or registrationPage or a different page will go into the @MethodSource as argument parameters and from the @MethodSource to the ParameterizedTest.

        I have a RegistrationPage in which I have the getFullDomain()

        
        public class RegistrationPage {
            public String getFullDomain(){
                return "www.sample.com"
            }
        }
        
        

        This BasePage below has the RegistrationPage object created and @Autowired so I can access the RegistrationPage getFullDomain() method using the BasePage

        
        public class BasePage{
        @Autowired 
        RegistrationPage registrationPage;
        }
        
        

        My ParameterizedTest Class below is extending the BasePage so I can use the methods in registrationPage without creating an instance for the class separately in the test below.

        
        public class RegistartionTest extends BasePage{
        
        public String domain= registrationPage.getFullDomain();
        public String email= registrationPage.getEmail();
        public String password= registrationPage.getPassword();
        
         Stream registrationInputParameters() {
                return Stream.of(
                        Arguments.of(domain,  email, password)
                );
            }
        
            @ParameterizedTest()
            @MethodSource("registrationInputParameters")
            public void sampleTest(String domain, String email, String password) {
                RegistrationPage registrationPage= open(fullDomain);
        
                registrationPage.register(email, password);
        }
        }
        
        

        When I do this and access the getFullDomain() method in my @MethodSource it returns null. And your document on the @MethodSource above says we can't pass external parameters into the @MethodSource. Just wondering if we have any solution for it.

        Thank you.

        • Petri Oct 26, 2019 @ 19:57

          Hi,

          I noticed two things:

          • It seems that you want to inject the RegistrationPage bean into the BasePage bean by using field injection. However, the BasePage class isn't annotated with a bean annotation. Also, your test class doesn't load the Spring application context. If you use this configuration, the value of the registrationPage field is null when you run your test.
          • Based on your test class, you seem to use the default lifecycle configuration. If this is the case, the registrationInputParameters() must be static.
  • Dheeraj Mar 20, 2020 @ 13:09

    how can we pass string formatted using String.format() as value

  • NightFox Aug 18, 2020 @ 22:04

    I want to do for the json file similar to display in above examples for csv, but in my case I do have multiple json having different keys in each json. I want to extract single key example: key1 from each json and display that key value to parameterisedTest annotation dynamically.

  • Nightking Aug 18, 2020 @ 22:07

    @ParameterizedTest
    @JsonFileSource(resource="file1.json")
    public void sampleTestMethod(JsonObject obj){
    }
    }

    inside file1.json , I do have multiple jsons and I want to extract key1 from each json and display that key value to the @ParameterizedTest dynamically to show test case name at run time with that value.(key1 holding test case name)

    • Petri Sep 10, 2020 @ 17:23

      Hi,

      First, I am sorry that it took me a long time to answer to your question. I just get so many comments nowadays that sometimes I miss one or two.

      Second, unfortunately I don't know how you can fulfill your requirement. I hope that you managed to find an answer to your problem though.

    • Everson Mauda Nov 25, 2020 @ 16:28

      Hi,

      You can use this dependency:

      http://www.joshka.net/junit-json-params/

      Regards!

      Everson Mauda (www.mauda.com.br)

    • Bhuvan Nov 8, 2023 @ 19:39

      Do we have a solution for it? I am facing the same requirement.

  • Shahin Aug 31, 2021 @ 10:54

    Hello Petri
    I am so glad I found your tutorial, and really appreciate the references to the documentation. You also covered various ways of Parameterized testing with enough detail and explanation that I can follow. Thank you. I have a question. I want to create and populate a tree with data, and then test methods that use the populated tree to get the answer. In order to do this, I need to create two other object types: Point2D, and RectHv, and I like to be able to create different number of each and different instances for different tests.
    So far I have looked into using the @TestInstance(TestInstance.Lifecycle.PER_CLASS) attribute as well as different ParameterizedTesting annotation tags but have not had any success. I am working on the following factory method but have not figured out how to convert the stream I have to one of type . I also get a compiler message saying "the stream is of type void"; my stream that is. The one I created. Here is what I have ...

    
    Stream createKdTreeInstance() throws IOException {
            Stream stream;
            System.out.println("Inside the createdKdTreeInstance Method ");
            stream = Files.lines(Paths.get("src/main/resources/distinctpoints.txt"));
            kt = new KdTree();
            stream.map(x -> x.split("\\s")).forEach(x -> kt.insert(new Point2D(Double.parseDouble(x[0]),
                    Double.parseDouble(x[1]))));
        }
    
        @DisplayName("range() Method Test")
        @CsvSource({".082,0.5,0.084,0.52"})
        @MethodSource("createKdTreeInstance")
    
        void range(double a, double b, double c, double d) throws IOException {
            createKdTreeInstance();
            RectHV r = new RectHV(a, b, c, d);
            Assertions.assertNotNull(kt.range(r));
        }
    
    

    I am still reading about streams, and need to read the links you provided above. Again, do not know how to thank you for creating this content, and taking the time to provide the links. Before I found your site, I was thinking I wish the articles on Parametrized Testing provided a bit more details and / or covered other supported features. Most were a bit over what I could follow. thank you so much.

  • Shahin Aug 31, 2021 @ 18:42

    I wanted to post an update since I have made progress, but run into an error stating "No ParameterResolver registered for parameter [edu.princeton.cs.algs4.RectHV arg1] in method [void KdTreeTest.range(KdTree,edu.princeton.cs.algs4.RectHV,edu.princeton.cs.algs4.Point2D[])]." I implmented an AgumentProvider as follows:

    
    class KdTreeTest {
        static class KdTreeArgumentsProvider implements ArgumentsProvider {
            // read the data for each test from the files in a directory that contains the points, rectangle(s), and the expected result(s)
            // create the kdtree and add points to it by reading two double values from a file. I wonder how I should design the
            // input data file. Ideally I want all the points first, then I want to create a rectangle, but it would be nice to
            // have one file that results in multiple test instances with the same KdTree but different rectangles and expected
            // results
            KdTree kt = new KdTree();
            // create a rectangle by reading four double values from a file. May be possible to put the data in the same file
            // that way one file has all the data for one test instance
            RectHV r = new RectHV(0.1, 0.1, 0.5, 0.6);
            // Just adding two points to expected points for now, but it can be expanded.
            Point2D p1 = new Point2D(0.1, 0.2);
            Point2D p2 = new Point2D(0.8, 0.9);
            Point2D[] expectPoints = {p1, p2};
            // pass the tree instance and the rectangle along with the expected results to a test method to validate the result
            // the expected results are a set of Points
            @Override
            public Stream provideArguments(ExtensionContext extensionContext) throws Exception {
                // here is how to return KdTree instance and the rectangle.
                return Stream.of(kt, r, expectPoints).map(Arguments::of);
            }
        }
        @DisplayName("should create a rectangle with the given coordinates and test KdTree's range() function")
        @ParameterizedTest(name = "{index}=> kt={0},r={1},expectedPoints={2}")
        @ArgumentsSource(KdTreeArgumentsProvider.class)
        void range(KdTree kt, RectHV r, Point2D[] expectedPoints) {
            Assertions.assertNotNull(kt.range(r));
        }
    }
    
    
    • Petri Sep 8, 2021 @ 18:54

      Hi,

      First, I am sorry that it took me some time before I noticed your comment.

      The problem is found from the provideArguments() method of the KdTreeArgumentsProvider class.

      When you invoke the of() method of the Stream class, you create a stream which has three elements (kt, r, and expectedPoints). Also, because you use the map() method of the Stream class, each element is wrapped inside an Arguments object. In other words, the returned stream contains three Arguments objects which have one argument per object. Because your parameterized test has three arguments and your provider provides only one, your test fails.

      You can fix this problem by changing the implementation of the provideArguments() method:

      
      return Stream.of(Arguments.of(kt, r, expectPoints)); 
      
      

      I hope that this solves your problem.

      • Shahin Oct 6, 2021 @ 23:09

        Hello and thank you so much for the reply. I'll try the solution.

Leave a Reply