Enhance your JUnit tests with parametrization

Aleksandra Bieńkowska

Have you ever found yourself in a situation where you needed to execute the same JUnit test with varying sets of arguments? In such cases, JUnit comes to the rescue providing Parametrized Tests — a convenient solution for running a single test with arguments from different sources. To implement Parametrized tests, we utilize the @ParameterizedTest annotation and specify the source of the arguments. Every execution of a parametrized test follows the same lifecycle as a typical @Test method. For instance, the @BeforeEach method is executed before each invocation. These invocations are sequentially displayed in the test tree of an IDE. Let’s delve into practical examples to take a closer look at how to leverage Parametrized Tests in practice.

Use cases of the parametrized tests

To initiate the usage of parametrized tests, it is necessary to include a dependency on the junit-jupiter-params artifact. For the context of this article, we will employ the following Maven declaration:

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

For the purposes of this article, the subsequent method has been developed. This method performs the automated assessment of task complexity based on multiple parameters.

public TaskLevel assessTaskLevel (int subtasksNumber,
                                  int avgResolutionTimeInMinutes,
                                  int positionInTasksRank,
                                  String taskLabel,
                                  TaskLevel avgUsersOpinion) {
    if (subtasksNumber > 10 || avgTimeInMinutes > 90 
                            || avgUsersOpinion == TaskLevel.HIGH) {
        return TaskLevel.HIGH;
    } else if (subtasksNumber > 5 && positionInTasksRank < 10 
                            && "M".equals(taskLabel)) {
        return TaskLevel.MEDIUM;
    } else {
        return TaskLevel.LOW;
    }
}

Parameterizing a singular test argument using the @ValueSource annotation

To start, let’s test the assessment method against a single parameter, avgResolutionTimeInMinutes. To accomplish this, we’ll employ the @ValueSource annotation, which enables us to specify a single array of literal values. This annotation allows us to provide only a single argument for each parameterized test invocation, in this scenario, referring to avgResolutionTimeInMinutes. The values of the remaining method arguments are fixed. The test method looks as follows:

@ParameterizedTest
@ValueSource(ints = {91, 100, 150, 200, Integer.MAX_VALUE})
void shouldTestBeAssessedAsHigh(int avgResolutionTime) {
    int subtasksNumber = 8;
    int positionInRank = 3;
    String taskLabel = "B";
    TaskLevel avgUsersOpinion = TaskLevel.MEDIUM;

    assertEquals(TaskLevel.HIGH, 
        TaskAssessment.assessTaskLeve(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion)
    );
}

Empty and null parameters

JUnit provides a convenient means to test against null and empty values through annotations such as @NullSource@EmptySource@NullAndEmptySource. These annotations can be used to parameterize the test with various values for the taskLabel argument, as illustrated below:

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"B","C","D"})
void shouldTaskBeAssessedAsHigh(String taskLabel) {
    int subtasksNumber = 8;
    int positionInRank = 3;
    int avgResolutionTime = 100;
    TaskLevel avgUsersOpinion = TaskLevel.MEDIUM;

    assertEquals(TaskLevel.HIGH, 
        TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion)
    );
}

In this instance, we utilize the default INCLUDE mode; however, alternative modes such as EXCLUDE or MATCH_ALL are also available.

Parameterizing tests with multiple arguments using the @MethodSource annotation

Until now, we have been parameterizing tests with a single argument. What if we intend to parameterize a test with multiple arguments? Let’s examine the following example:

@ParameterizedTest
@MethodSource("provideTaskAssessmentArguments")
void shouldTaskBeAssessed (int subtasksNumber,
                            int positionInRank,
                            int avgResolutionTime,
                            String taskLabel,
                            TaskLevel avgUsersOpinion,
                            TaskLevel expectedResult) {
    assertEquals(expectedResult, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

static Stream<Arguments> provideTaskAssessmentArguments() {
    return Stream.of(
            arguments(8, 3, 60, "M", TaskLevel.MEDIUM, TaskLevel.MEDIUM),
            arguments(8, 3, 60, "B", TaskLevel.UPPER_LEVEL, TaskLevel.HIGH),
            arguments(8, 3, 60, "", TaskLevel.UPPER_LEVEL, TaskLevel.HIGH),
            arguments(8, 3, 60, null, TaskLevel.UPPER_LEVEL, TaskLevel.HIGH)
    );
}

In this instance, we use the provideTaskAssessmentArguments method as a source for various sets of arguments. The name of the method is specified in the @MethodSource annotation. This method returns a Stream of elements of the predefined Arguments class. It’s worth noting that instead of a Stream, we can utilize any data structure that can be converted into a Stream, such as a Collection, Iterable, or streams of primitive types like DoubleStream, IntStream, and so on.

The @MethodSource annotation can also be applied to parameterize tests with a single argument. In this scenario, instead of using the Arguments type in the Stream, other types like String can be employed:

@ParameterizedTest
@MethodSource("provideTaskLabels")
void shouldTaskBeAssessedByTaskLabel(String taskLabel) {
    int subtasksNumber = 8;
    int positionInRank = 7;
    int avgResolutionTime = 60;
    TaskLevel avgUsersOpinion = TaskLevel.HIGH;
    assertEquals(TaskLevel.HIGH, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

static Stream<String> provideTaskLabels() {
    return Stream.of("A", "B", "C");
}

Parameterizing tests with multiple arguments using the @ArgumentsSource annotation

The same Stream of arguments, as described above, can be supplied from a method implemented in a separate class by utilizing the @ArgumentsSource annotation, as illustrated below:

public class TestAssessmentArgumentsProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
            arguments(8, 5, 60, "M", TaskLevel.MEDIUM, TaskLevel.MEDIUM),
            arguments(8, 3, 60, "B", TaskLevel.UPPER_LEVEL, TaskLevel.HIGH),
            arguments(8, 3, 60, "", TaskLevel.UPPER_LEVEL, TaskLevel.HIGH),
            arguments(8, 3, 60, null, TaskLevel.UPPER_LEVEL, TaskLevel.HIGH)
        );
    }
}

@ParameterizedTest
@ArgumentsSource(TestAssessmentArgumentsProvider.class)
void shouldTaskBeAssessedWithParameters(int subtasksNumber,
                                        int positionInRank,
                                        int avgResolutionTime,
                                        String taskLabel,
                                        TaskLevel avgUsersOpinion,
                                        TaskLevel expectedResult) {

    assertEquals(expectedResult, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

The implementation of ArgumentsProvider must be declared either as a top-level class or as a static nested class.

Parameterizing a test using the @CsvSource annotation

Another option for parameterizing a test with multiple arguments is to utilize the @CsvSource annotation. This annotation enables us to represent argument lists as delimiter-separated values, with the default delimiter being a comma. Each string provided in the value attribute of @CsvSource represents a CSV record and corresponds to one test invocation. In the example below, the same set of arguments as before is provided using the @CsvSource:

@ParameterizedTest
@CsvSource({
        "8, 5, 60, 'M', 'MEDIUM', 'MEDIUM'",
        "8, 3, 60, 'B', 'UPPER_LEVEL', 'HIGH'",
        "8, 3, 60, '', 'UPPER_LEVEL', 'HIGH'",
        "8, 3, 60,, 'UPPER_LEVEL', 'HIGH'"
})
void shouldTaskBeAssessedWithParameters(int subtasksNumber,
                                        int positionInRank,
                                        int avgResolutionTime,
                                        String taskLabel,
                                        TaskLevel avgUsersOpinion,
                                        TaskLevel expectedResult) {

    assertEquals(expectedResult, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

Note that string values are enclosed in single quotes (an empty string is represented as ”), and the absence of a value between delimiters signifies a null value.

Supplying test parameters from a file using the @CsvFileSource annotation

JUnit enables the definition of lists of comma-separated arguments in files located in the classpath or the local file system. To achieve this, we can utilize the @CsvFileSource annotation, as demonstrated in the example below:

@ParameterizedTest
@CsvFileSource(files = "src/test/resources/arguments.csv")
void shouldTaskBeAssessedWithParameters(int subtasksNumber,
                                        int positionInRank,
                                        int avgResolutionTime,
                                        String taskLabel,
                                        TaskLevel avgUsersOpinion,
                                        TaskLevel expectedResult) {

    assertEquals(expectedResult, 
    TaskAssessment.assessTaskLevel(subtasksNumber, avgResolutionTime, positionInRank, taskLabel, avgUsersOpinion));
}

The arguments values have been organized in a separate .csv file:

8, 5, 60, "M", "MEDIUM", "MEDIUM",
8, 3, 60, "B", "UPPER_LEVEL", "HIGH",
8, 3, 60, "", "UPPER_LEVEL", "HIGH",
8, 3, 60,, "UPPER_LEVEL", "HIGH"

Notice that this time we use double quotes for characters sequence notation. An unquoted empty value is converted to a null.

Summary

In this article, we’ve gained an overview of how to parameterize tests with JUnit. JUnit 5 offers a comprehensive toolkit of methods and annotations for convenient test parameterization with various types of sources.

Reference

Meet the geek-tastic people, and allow us to amaze you with what it's like to work with j‑labs!

Contact us