AdBlock Detected

It looks like you're using an ad-blocker!

Our team work realy hard to produce quality content on this website and we noticed you have ad-blocking enabled. Advertisements and advertising enable us to continue working and provide high-quality content.

Parametized test with JUnit5

The arrival of JUnit 5 brought some improvements and innovations, such as Parametized Test with JUnit 5. This new functionality allows us to execute a test multiple times with different parameters.

In this article, we will focus on the use of Parametized Tests for testing our application. In a previous article, you can find more examples of testing with Spring Boot.

What is JUnit?

JUnit is a set of libraries that allows us to perform testing of our Java applications.

JUnit 5 is the latest generation of JUnit, which brings a large number of features, including the use of Parametized Tests.

Maven Dependency for Using Parametized Test with JUnit5

The dependency we use to add JUnit 5 is:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>${version}</version>
    <scope>test</scope>
</dependency>

If we are using Spring Boot, the JUnit 5 dependency is already included.

Why Use Parametized Tests?

Parametized Tests in JUnit 5 allow us to add different values with a simple annotation to perform tests on a method.

For example, let’s say we have a method that calculates whether a number is even or odd, and we want to test it with several numbers. In that case, we would use Parametized Tests, as it allows us to pass a range of numbers.

For example:

public class Calculate {
    public static boolean isEven(int number) {
        return number % 2 == 0;
    }
}

@ParameterizedTest
@ValueSource(ints = {0, 2, 4, 6}) 
void given_4_even_numbers_should_return_true(int number) {
    assertTrue(Calculate.isEven(number));
}

The above test (the @Test annotation is not necessary) will execute sequentially and in order the values specified in @ValueSource.

Uses of Parametized Tests with JUnit 5

Depending on our needs and how we want to perform our tests and load different values, we can do it in different ways.

Let’s perform tests on the following method:

Using Parametized Tests with Null and Empty

When we are testing our methods, it is vital to validate whether our application will work correctly with null and/or empty values. To perform these tests, we can pass a null value and an empty value using annotations.

public class User {

    private static final String name = "noel";
    
    public static String concat(String surname) {

        if (surname == null || surname.isEmpty()) {

           return name;
        }
        return name + " " + surname;
    }
}

Verify Null Value with Parametized Test

Let’s make use of the @NullSource annotation to pass a null value to the generated method above.

In the case where we receive a null parameter, we will return only the name; otherwise, we will perform concatenation.

@ParameterizedTest
@NullSource
void given_null_surname_then_return_name(String surname) {
    assertTrue(User.concat(surname).equalsIgnoreCase("noel"));
}

Check emtpy value with Parametized Test

Another annotation that JUnit 5 offers us along with the functionality of Parametized Test is @EmptySource, which will pass an empty value as a parameter. If we take into account the previously created concat method, we could perform a test in the following way:

@ParameterizedTest
@EmptySource
void given_empty_surname_then_return_name(String surname) {
    assertTrue(User.concat(surname).equalsIgnoreCase("noel"));
}

The previous test will allow us to pass an empty value as a parameter in order to perform tests.

Uso de empty y Null con una anotación con Parametized Test

In the previous points, we have seen two annotations that allow us to pass null or empty values. With the annotation @NullAndEmptySource, we can combine both.

@ParameterizedTest
@NullAndEmptySource
void given_empty_surname_then_return_name(String surname) {
    assertTrue(User.concat(surname).equalsIgnoreCase("noel"));
}

Usage of Parametized Test with @ValueSource

In the first example we saw of Parametized Test, we learned how to pass values using the annotation @ValueSource. In the following points, we will see some more examples and the data types that can be used.

It’s important to note that we cannot pass null values. To handle this, as we saw in the previous point, we can use @NullSource.

We can only use the following data types:

  • short
  • byte
  • int
  • long
  • float
  • double
  • char
  • java.lang.String
  • java.lang.Class

If we create a test where we pass an empty value, a space, and a last name to the previously created concat method, we can do something like this:

@ParameterizedTest
@ValueSource(strings = {"", "  ", "rodriguez"})
void given_values_then_return_ok(String surname) {
    assertNotNull(User.concat(surname));
}

CSV with Parametized Test

One of the possibilities offered by the use of Parametized Test is the ability to apply CSV formats to our tests. This way, we can load a large amount of data to perform tests and ensure the proper functioning of our applications.

To use CSV in our tests, we can use two different annotations:

  • @CsvSource: Loads the data from literals passed with the annotation.
  • @CsvFileSource: Loads the data from a file.

Usage of @CsvSource in Parametized Test

With the use of @CsvSource, we will pass a key-value pair separated by a “comma” as the default value, although we can use any other separator as long as we indicate it. Let’s see how it works.

Let’s create a method where the sum of two to the first parameter is equal to the second parameter.

public class Test{
    public static int plusNumber(int number) {
        return number + 2;
    }
}
@ParameterizedTest
@CsvSource({"2,4", "6,8", "10,12"})
void given_a_number_when_plus_then_should_generate_the_expected(int input, int expected) {
    int value= Test.plusNumber(input);
    assertEquals(expected, value);
}

If we want to change the comma to a colon, for example, we can add the parameter delimiter:

@ParameterizedTest
@CsvSource(value = {"2:4", "6:8", "10:12"}, delimiter = ':')
void given_a_number_when_plus_then_should_generate_the_expected(int input, int expected) {
    int value= Test.plusNumber(input);
    assertEquals(expected, value);
}

As we can see in the previous test, what we have done is to pass two values through the input and expected parameters, so that we can verify our method.

Using CSV files with Parametized Test

We can also load values from a CSV file instead of directly providing them in the method.

To do this, all we need is to create a CSV file with two columns, where the column headers will carry the names of the input parameters. We can also include the parameter numLinesToSkip = 1 to skip the header.

Let’s see an example:

input,expected
test,TEST
tEst,TEST
Java,JAVA
@ParameterizedTest
@CsvFileSource(resources = "/numbers.csv")
void given_a_number_when_plus_then_should_generate_the_expected(int input, int expected) {
    int value= Test.plusNumber(input);
    assertEquals(expected, value);
}

Using Enum with Parametized Test

Continuing with test automation, we can use the @EnumSource annotation to assign values when working with enums. For example:

@ParameterizedTest
@EnumSource(DayOfWeek.class) 
void given_week_value_are_bettwen_1_and_7(DayOfWeek week) {
    int day= week.getValue();
    assertTrue(day>= 1 && day<= 7);
}

DayOfWeek is an enum from java.time.

When running the previous test, it will be executed once for each value of the enum.

Using methods to verify tests with Parametized Test

Another new feature that came with JUnit 5 and Parametized Tests is the ability to pass methods to verify our tests. Since the examples we have seen so far only allow us to pass simple values, let’s see how we can use @MethodSource to invoke a method to return arguments.

The values that the invoked method can return are the following:

  • Stream
  • Iterable
  • Iterator
  • Array of arguments

Let’s see an example. Imagine we have a class to get the day of the week associated with a number, and we want to analyze if the relationship is correct:

@ParameterizedTest
@MethodSource("numberToDay")
void numberToDay(int day, String name) {
    assertEquals(name, DayOfWeek.of(day));
}

private static Stream<Arguments> numberToDay() {
    return Stream.of(
            arguments(1, "Sunday"),
            arguments(2, "Monday"),
            arguments(3, "Tuesday")
            arguments(4, "Wednesday"),
            arguments(5, "Thursday"),
            arguments(6, "Friday"),
            arguments(7, "Saturday")

    );
}

In the previous code snippet, it will validate that the parameter number matches the expected number.

If we don’t pass the method name, by default it will look for a matching name, in this case, it would be numberToDay.

Sharing Arguments with @ArgumentSource

With @MethodSource, we can also refer to other methods. To do this, we need to make the full method call, including the package:

package com.refactorizando;

import java.util.stream.Stream;

public class UpperString{
    private static Stream<String> letters() {
        return Stream.of("AAA", "BBB", "CCC");
    }
}

To fully call the method, you need to provide the complete combination of package name, method, and class.

The method areEqualsAndUpper is responsible for verifying that the letters are equal and in uppercase.

@ParameterizedTest
@MethodSource("com.refactorizando.UpperString#letters")
void equalsLettersAndUpper(String string) {
    assertTrue(StringUtils.areEqualsAndUpper(string));
}

Although the previous option is valid, it is more recommended to write a custom class that implements ArgumentsProvider:

public class EqualsLetterProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of("AAA", "BBB", "CCC").map(Arguments::of);
    }
}

Once we have created the aforementioned class, we can assign it to the @ArgumentsSource annotation.

@ParameterizedTest
@ArgumentsSource(EqualsLetterProvider.class)
void equalsLettersAndUpper(String string) {
    assertTrue(StringUtils.areEqualsAndUpper(string));
}

Convert arguments with Parametized Test

One of the features of JUnit 5 is the ability to transform values from a String to an enum using a type converter. For more information and the types that can be converted implicitly, you can take a look at JUnit 5’s type conversion.

Thanks to implicit type conversion, we can do something like this:

@ParameterizedTest
@CsvSource({"MONDAY", "TUESDAY", "WEDNESDAY"}) 
void given_week_value_are_bettwen_1_and_7(DayOfWeek week) {
    int day= week.getValue();
    assertTrue(day>= 1 && day<= 7);
}

Create argument converter with Parametized Test

JUnit 5 provides only a few specific conversions, so for some others, we need to create a specific converter. Therefore, whenever we use the specific converter we have created, we will do it with the annotation @ConvertWith.

Let’s create a converter to convert from String to int, but only if the value is a number.

To create a custom converter, we can use the ArgumentConverter class or the TypedArgumentConverter class, which we will use in our case, as we are working with a specific type.

class StringToIntConverter extends TypedArgumentConverter<String, Integer> {
    protected StringToIntConverter () {
        super(String.class, Integer.class);
    }

    @Override
    public Integer convert(String source) throws ArgumentConversionException {
        try {
            return Integer.parseInt(source);

        } catch (NumberFormatException e) {
            throw new ArgumentConversionException("Cannot convert string to int", e.getMessage());
        }
    }
}

To test our type converter, we do it in the following way:

@ParameterizedTest
@CsvSource({
        "1, 1",
        "1, 1",
        "2, 2"
})
void convertWithCustomHexConverter(int number, @ConvertWith(StringToIntConverter.class) int result) {
    assertEquals(number, result);
}

Customizing names in the execution of tests with Parametized Test in JUnit 5

When we execute our tests with JUnit 5, they appear with the invocation index and a string representation of all the parameters. However, if we want to modify this representation, we can do so by adding arguments to ParametizedTest to specify how our tests will be displayed:

@ParameterizedTest(name = "{index} => number={0}, day={1}")
@MethodSource
void numberToDay(int day, String name) {
    assertEquals(name, DateWeekUtils.getWeekDay(day));
}

private static Stream<Arguments> numberToDay() {
    return Stream.of(
            arguments(1, "Sunday"),
            arguments(2, "Monday"),
            arguments(3, "Tuesday")
            arguments(4, "Wednesday"),
            arguments(5, "Thursday"),
            arguments(6, "Friday"),
            arguments(7, "Saturday")

    );
}

With the parameterization we have done, passing the number and the day as arguments, we will obtain the test results in a more organized manner.

 monthNames(int, String)
├─ 1 => number=1, month=Sunday
├─ 2 => number=2, month=Monday
├─ 3 => number=3, month=Tuesday
├─ 4 => number=4, month=Wednesday
├─ 5 => number=5, month=Thursday
├─ 6 => number=6, month=Friday
└─ 7 => number=7, month=Saturday

Conclusion

In this article about using Parametized Test with JUnit 5, we have seen various examples of its usage and how it can help us perform automated testing.

This new feature of JUnit 5 is very helpful in adding greater verification to our tests and ensuring that our applications function correctly.

If you need more information, you can leave us a comment or send an email to refactorizando.web@gmail.com You can also contact us through our social media channels on Facebook or twitter and we will be happy to assist you!!

Leave a Reply

Your email address will not be published. Required fields are marked *