Junit 5 : Write Powerful Unit Test Cases Using Parameterized Tests

unsplash-logoWellington Rodrigues

JUnit 5

Unlike previous versions of JUnit. JUnit 5 is complete rewrite and has lot of interesting architecture changes. JUnit 5 is not Single project but compose from three sub-projects: Jupiter, Vintage, and Platform.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

Check JUnit 5 User Guide For more details. You can find all code samples in on my GitHub Account

In this post We will discuss Parameterized Tests in JUnit 5.

Parameterized Tests

JUnit 5 Parameterized Test are very powerful. With the help of Parameterized Test We can remove the duplication in test cases.Parameterized test cases can also help us to cleanup the test code and remove the cluter.
As Name suggest Parameterized tests make it possible to run a test multiple times with different arguments. They are declared just like regular @Test methods but use the @ParameterizedTest annotation instead. In addition, you must declare at least one source that will provide the arguments for each invocation and then consume the arguments in the test method.

Simple ParameterizedTest

The following example demonstrates a parameterized test that uses the @ValueSource annotation to specify a String array as the source of arguments. Test Case will be called 2 times with parameter Hello and World.
Framework will be responsible for injecting parameter values

Simple ParameterizedTest
1
2
3
4
5
6
7
   @ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void withSomeValues(String word) {
System.out.println(word);
assertNotNull(word);

}

Display Names For ParameterizedTest

JUnit 5 has added new annotation @DisplayName, Which helps to provide more readable names to test classes and methods.These names will be displayed by test runners and test reporting.

Display Names
1
2
3
4
5
6
7
8
   
@DisplayName("String should not be null")
@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void withSomeValues(String word) {
System.out.println(word);
assertNotNull(word);
}

But in case of Parameterized Test,Sometimes we might need to name test cases based on arguments. JUnit 5 provides index and arguments variable for this

Display Names For ParameterizedTest
1
2
3
4
5
6
7
8
   
@ParameterizedTest(name = "Null Check Test #{index} with [{arguments}]")
@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void withSomeValues(String word) {
System.out.println(word);
assertNotNull(word);
}

Sources of Arguments

Out of the box, JUnit Jupiter provides number of Argument Source providers.

  1. @ValueSource
  2. @EnumSource
  3. @MethodSource
  4. @CsvSource
  5. @CsvFileSource
  6. @ArgumentsSource

Let’s try one by one

@EnumSource

@EnumSource provides a convenient way to use Enum constants. This annotation provides an optional names parameter that lets you specify which constants shall be used. If omitted, all constants will be used like in the following example.

EnumSource
1
2
3
4
5
6
7
    @ParameterizedTest(name = "withSomeName #{index} with Value [{arguments}]")
@EnumSource(MyTestEnum.class)
void withSomeEnum(MyTestEnum myEnum) {
System.out.println(myEnum);
assertNotNull(myEnum);
}

@MethodSource

@MethodSource allows you to refer one or more factory methods of the test class or external classes.

Method Source in Same class

  • Factory methods within the test class must be static.
  • Each factory method must generate a stream of arguments.
MethodSource in Same Class
1
2
3
4
5
6
7
8
9
10
11
   @ParameterizedTest
@MethodSource("createWordsWithLength")
void withMethodSource(String word, int length) {
System.out.println("withMethodSource");
assertNotNull(word);
}

private static Stream<Arguments> createWordsWithLength() {
return Stream.of(Arguments.of("Hello", 5), Arguments.of("JUnit 5", 7));
}

Method Source in Other class

  • Factory methods in external classes must be static.
  • Each factory method must generate a stream of arguments.
  • Factory methods must not accept any arguments.
    MethodSource in external classes
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
       @ParameterizedTest
    @MethodSource("com.niraj.MethodSource#stringProvider")
    void withMethodSource(String word, int length) {
    System.out.println("withMethodSource");
    assertNotNull(word);
    }

    private static Stream<Arguments> createWordsWithLength() {
    return Stream.of(Arguments.of("Hello", 5), Arguments.of("JUnit 5", 7));
    }
    @CsvSource
    As name suggest @CsvSource allows you to express argument lists as comma-separated values (i.e., String literals).
    CsvSource
    1
    2
    3
    4
    5
       @ParameterizedTest
    @CsvSource({ "Hello, 5", "World, 5", "test,4" })
    void withCsvSource(String word, int length) {
    assertEquals(word.length(), length);
    }
    @CsvFileSource
    Similar to @CsvSource We can also provide csv values using file from classpath @CsvFileSource.
    CsvFileSource
    1
    2
    3
    4
    5
       @ParameterizedTest
    @CsvFileSource(resources = "/testdata.csv")
    void withCsvFileSource(String word, int length) {
    assertEquals(word.length(), length);
    }

ArgumentsSource

If any of the above Source provider does not meet your requirement, then you can use your custom Argument Source provider. You will need to implement ArgumentsProvider Interface.

ArgumentsSource
1
2
3
4
5
6
    @ParameterizedTest
@ArgumentsSource(StringArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
}
StringArgumentsProvider
1
2
3
4
5
6
public class StringArgumentsProvider implements ArgumentsProvider {

@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("Hello", "world").map(Arguments::of);
}

Argument Conversion

In all above Test cases you might have observed that the arguments are getting converted to method parameter types. In all examples, arguments are getting converted to String.

Who is converting these arguments ?

What if we change it to int or any other types?

What happens if incase we want to use any User Defined object ?

Widening Conversion
JUnit Jupiter supports Widening Primitive Conversion for arguments supplied to a @ParameterizedTest. For example, a parameterized test annotated with @ValueSource(ints = { 1, 2, 3 }) can be declared to accept not only an argument of type int but also an argument of type long, float, or double.

Explicit Conversion
We Can Specify ArgumentConverter to convert to any user define object. In below example i wanted to convert String “niraj,sonawane” to Person object.

ArgumentConverter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    @ParameterizedTest
@ValueSource(strings ={"niraj,sonawane"})
void withCustomConverter(@ConvertWith(PersonConverter.class) Person person) {
assertEquals(Person.getFirstName(),"niraj");
assertEquals(Person.getLastName(),"sonawane");
}
//Convert class
public class PersonConverter implements ArgumentConverter {
@Override
public Object convert(Object source, ParameterContext context) throws ArgumentConversionException {
if (source instanceof String)
try {
String[] split = ((String) source).split(",");
return new Emp(split[0], split[1]);

} catch (NumberFormatException ex) {
String message = source + " is no correct string representation of a Emp.";
throw new ArgumentConversionException(message, ex);
}
throw new ArgumentConversionException(source + " is no valid point");
}
}
Share Comments