In this article, we will see what is the Junit 5 Parameterized test, and examples on how to create parameterized test cases, several kind of argument sources for parameterized tests supported by Junit5, argument conversion and arguments aggregators.
Content :
- JUnit 5 Parameterized Test
- Argument sources for parameterized tests
- Argument conversion
- Argument Aggregation for Junit 5 Parameterized tests
1. Junit 5 Parameterized Test
Some times we may need to run same tests with different arguments or values, Junit 5 Jupiter Parameterized tests makes 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 of @Test
annotation.
1.1. Writing parameterized tests
Following are the steps to create parameterized tests in Junit 5.
- Declare
@ParameterizedTest
to the test. - Declare at least one source (example –
@ValueSource
) that will provide the arguments for each invocation of test. - Consume the arguments in the test method .
JUnit 5 parameterized test maven dependency : We need junit-jupiter-params
maven dependencies to support parameterized tests.
<!-- Jupiter API for writing tests --> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>${junit.jupiter.version}</version> <scope>test</scope> </dependency> <!-- Parameterized Tests --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>${junit.jupiter.version}</version> <scope>test</scope> </dependency> </dependencies>
public class Junit5_Parameterized_Test { // This test will run sequentially 5 times with 1 argument each time @ParameterizedTest @ValueSource(ints = {8,4,2,6,10}) void test_int_arrays(int arg) { System.out.println("arg => "+arg); assertTrue(arg % 2 == 0); } }
Output :
arg => 8 arg => 4 arg => 2 arg => 6 arg => 10
Test results in eclipse :
1.2. Customizing Display Names in Parameterized tests
By default, the display name of a parameterized test invocation contains the invocation index and the String
representation of all arguments for that specific invocation (see above image for reference). The following placeholders are supported within custom display names.
Placeholder | Description |
---|---|
{displayName} | placeholder for the display name of the method. |
{index} | placeholder for the current invocation index (starts from 1). |
{arguments} | placeholder for the complete, comma-separated arguments list. |
{0} , {1} , … | placeholder for an individual argument. |
public class Junit5_CustomDisplayNames_Test { @ParameterizedTest(name="#{index}- Test with Argument={arguments}") @ValueSource(ints = {8,4,2,6,10}) void test_int_arrays(int arg) { System.out.println("arg => "+arg); assertTrue(arg % 2 == 0); } @ParameterizedTest(name="#{index} - Test with Argument={0}") @ValueSource(strings = {"Peter King", "Arthur King", "Martin King"}) void test_string_arrays(String arg) { String searchKey = "King"; System.out.println("arg => "+arg); assertTrue(arg.contains(searchKey)); } @ParameterizedTest(name="Test with argument - {arguments}") @NullSource void test_null_source(String arg) { System.out.println("arg => "+arg); assertTrue(arg == null); } }
Test Results in eclipse :
2. Argument sources for parameterized tests
@ValueSource
@EnumSource
@MethodSource
@CsvSource
@CsvFileSource
@ArgumentSource
2.1. @ValueSource arguments
2.1.1. @ValueSource
lets you specify a single array of literal values and can only be used for providing a single argument per parameterized test invocation.
2.1.2. We can pass empty or null
values into the test via @EmptySource
, @NullSource
or @NullAndEmptySource
(since JUnit 5.4).
2.1.3. Only the following types of literal values are supported by @ValueSource
.
short
– useshorts
attribute to declare valuesbyte
– usebytes
attribute to declare valuesint
– useints
attribute to declare valueslong
– uselongs
attribute to declare valuesfloat
– usefloats
attribute to declare valuesdouble
– usedoubles
attribute to declare valueschar
– usechars
attribute to declare valuesjava.lang.String
– usestrings
attribute to declare valuesjava.lang.Class
– useclasses
attribute to declare values
public class Junit5_Valuesource_Test { @ParameterizedTest @ValueSource(ints = {8,4,2,6,10}) void test_int_arrays(int arg) { System.out.println("arg => "+arg); assertTrue(arg % 2 == 0); } @ParameterizedTest(name="#{index} - Test with Argument={0}") @ValueSource(strings = {"Peter King", "Arthur King", "Martin King"}) void test_string_arrays(String arg) { String searchKey = "King"; System.out.println("arg => "+arg); assertTrue(arg.contains(searchKey)); } @ParameterizedTest(name="#{index} - Test with Argument={0}") @NullSource void test_null_source(String arg) { System.out.println("arg => "+arg); assertTrue(arg == null); } @ParameterizedTest(name="#{index} - Test with Argument={0}") @EmptySource void test_empty_source(String arg) { System.out.println("arg => "+arg); assertTrue(arg.isEmpty()); } @ParameterizedTest @NullAndEmptySource @ValueSource(strings = { " ", " ", "\t", "\n" }) void test_nullAndEmpty_source(String arg) { assertTrue(arg == null || arg.trim().isEmpty()); } }
2.2. @EnumSource arguments
- To run a test with values from enumeration
@EnumSource
provides a convenient way to useEnum
constants. - The annotation’s
value
attribute is optional. When omitted, the declared type of the first method parameter is used. - The annotation provides an optional
names
attribute that lets you specify which constants shall be used to test. - By default, the
names
will only keep the matchedEnum
constants. We can turn this around by setting themode
attribute, which supportsEXCLUDE
,INCLUDE
,MATCH_ALL
,MATCH_ANY
.
public class Junit5_EnumSource_Test { enum Role { ADMIN, SUBSCRIBER, OBSERVER, AUTHOR, PUBLISHER, ANONYMOUS } @ParameterizedTest @EnumSource(Role.class) void testWith_EnumSource(Role role) { System.out.println("arg => "+role); assertNotNull(role); } @ParameterizedTest @EnumSource(value = Role.class, names = {"AUTHOR", "SUBSCRIBER"}) void testWith_EnumSource_include(Role role) { System.out.println("arg => "+role); assertNotNull(role); } @ParameterizedTest @EnumSource(value = Role.class, mode = Mode.EXCLUDE, names = {"AUTHOR", "SUBSCRIBER"}) void testWith_EnumSource_exclude(Role role) { System.out.println("arg => "+role); assertNotNull(role); } @ParameterizedTest @EnumSource(value = Role.class, mode = Mode.MATCH_ALL, names = "^(A|S).*R$") void testWith_EnumSource_Regex(Role role) { System.out.println("---------"); System.out.println("arg => "+role); assertNotNull(role); } }
2.3. @MethodSource arguments
- By using
@valueSource
or@EnumSource
it’s not possible to pass complex types as argument,@MethodSource
allow us a factory method of the test class or external classes provide complex objects in test arguments. - Factory methods should be
static
and must not accept any arguments. - Each factory method must generate a stream of arguments and each set of values will be provided to the arguments of test method.
- If you do not explicitly provide a factory method name via
@MethodSource
, JUnit Jupiter will search for a factory method that has the same name as the current@ParameterizedTest
method by convention.
public class Junit5_MethodSource_Test { @ParameterizedTest @MethodSource("stringProvider") void testWith_MethodSource(String arg) { System.out.println("testWith_MethodSource(arg) => "+arg); assertNotNull(arg); } @ParameterizedTest @MethodSource void testWith_Default_local_MethodSource(String arg) { System.out.println("testWith_Default_local_MethodSource(arg) => "+arg); assertNotNull(arg); } @ParameterizedTest(name="#{index} - Test with Argument={0},{1},{2}") @MethodSource("stringIntAndListProvider") void testWith_Multiple_MethodSource(String str, int num, List<String> list) { System.out.println("str => "+str+"; num =>"+num+"; list => "+list); assertTrue(str.length() >= 0); assertTrue(num >=1 && num <=3); assertTrue(list.size() >= 1); } @ParameterizedTest(name="#{index} - Test with Argument={0},{1},{2}") @MethodSource("com.javabydeveloper.parameterized.external.ExternalArgsProvider#nameIdRoleProvider") void testWith_Multiple_ExternalMethodSource(String str, int num, String role) { System.out.println("str => "+str+"; num =>"+num+"; role => "+role); assertTrue(str.length() >= 0); assertTrue(num >=1 && num <=3); assertTrue(!role.isEmpty()); } static Stream<String> stringProvider() { return Stream.of("admin", "subscriber", "author","anonymous"); } static Stream<String> testWith_Default_local_MethodSource() { return Stream.of("Peter", "Philip", "John"); } static Stream<Arguments> stringIntAndListProvider() { return Stream.of( arguments("Peter", 1, Arrays.asList("admin", "author")), arguments("John", 2, Arrays.asList("subscriber")), arguments("Philip", 3, Arrays.asList("Author", "Publisher")) ); } }
public class ExternalArgsProvider { static Stream<Arguments> nameIdRoleProvider() { return Stream.of( arguments("Peter", 1, "admin"), arguments("John", 2, "author"), arguments("Philip", 3, "subscriber") ); } }
Output :
testWith_Default_local_MethodSource(arg) => Peter testWith_Default_local_MethodSource(arg) => Philip testWith_Default_local_MethodSource(arg) => John testWith_MethodSource(arg) => admin testWith_MethodSource(arg) => subscriber testWith_MethodSource(arg) => author testWith_MethodSource(arg) => anonymous str => Peter; num =>1; role => admin str => John; num =>2; role => author str => Philip; num =>3; role => subscriber str => Peter; num =>1; list => [admin, author] str => John; num =>2; list => [subscriber] str => Philip; num =>3; list => [Author, Publisher]
2.4. @CsvSource arguments
@ValueSource
or@EnumSource
does not allow you to provide multiple parameters / arguments,@CsvSource
allows you to express argument lists as comma-separated values.- The
@CsvSource
accepts an array of comma-separated values and each array entry corresponds to a line in a CSV file.
public class Junit5_CSVSource_Test { @ParameterizedTest @CsvSource({ "Peter, admin, 1", "John, author, 2", "Martin, subscriber, 3" }) void testWith_CsvSource(String name, String role, long id) { System.out.println("testWith_CsvSource: name => "+name+"; role => "+role+"; id => "+id); assertTrue(name.length() >= 0); assertTrue(id >=1 && id <=3); assertTrue(!role.isEmpty()); } @ParameterizedTest @CsvSource({ "Peter, admin, 1", "John, author, 2", "Martin, , 3" }) void testWith_CsvSource_with_null(String name, String role, long id) { System.out.println("name => "+name+"; role => "+role+"; id => "+id); assertTrue(name.length() >= 0); assertTrue(id >=1 && id <=3); assertTrue(role != null); } // @CsvSource(value = { "Peter, NIL, 1" }, nullValues = "NIL") // above line provide arguments => Peter, null, 1 (Since Junit 5.6) }
Output :
testWith_CsvSource: name => Peter; role => admin; id => 1 testWith_CsvSource: name => John; role => author; id => 2 testWith_CsvSource: name => Martin; role => subscriber; id => 3 name => Peter; role => admin; id => 1 name => John; role => author; id => 2 name => Martin; role => null; id => 3
2.5. @CsvFileSource arguments
- If we have to provide large number of sets as arguments to set comma-separated values, @CsvFileSource lets you use CSV files from the
classpath
instead using@CsVSource
. Following are the CSV file values.
Name | Role | Id |
Peter | admin | 1 |
John | author | 2 |
Martin | subscriber | 3 |
public class Junit5_CSVFileSource_Test { @ParameterizedTest @CsvFileSource(resources = "/users-data.csv", numLinesToSkip = 1) void testWith_MethodSource(String name, String role, long id) { System.out.println("name => "+name+"; role => "+role+"; id => "+id); assertTrue(name.length() >= 0); assertTrue(id >=1 && id <=3); assertTrue(!role.isEmpty()); } }
Output :
name => Peter; role => admin; id => 1 name => John; role => author; id => 2 name => Martin; role => subscriber; id => 3
2.6. @ArgumentsSource – Custom Argument Provider
To support advanced approach, Junit 5 supports a custom, reusable ArgumentProvider
to provide test arguments.
Following is the Custom Argument provider.
public class CustomArgumentProvider implements ArgumentsProvider{ @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception { return Stream.of( arguments("Peter", 1, Arrays.asList("admin", "author")), arguments("John", 2, Arrays.asList("subscriber")), arguments("Philip", 3, Arrays.asList("Author", "Publisher")) ); } }
Using CustomArgumentProvider
in test case.
public class Junit5_ArgumentSource_Test { @ParameterizedTest @ArgumentsSource(CustomArgumentProvider.class) void testWith_implicit_conversion(String str, int num, List<String> list) { System.out.println("str => "+str+"; num =>"+num+"; list => "+list); assertTrue(str.length() >= 0); assertTrue(num >=1 && num <=3); assertTrue(list.size() >= 1); } }
Output :
str => Peter; num =>1; list => [admin, author] str => John; num =>2; list => [subscriber] str => Philip; num =>3; list => [Author, Publisher]
3. Argument conversion
3.1. Implicit conversion
JUnit Jupiter provides a number of built-in implicit type converters. The conversion process depends on the declared type of each method parameter. The conversion process depends on the declared type of each method parameter. For example if you provide parameterized test annotated with @ValueSource(ints = { 2, 5, 7 })
, method argument can be int
, long
, float
, or double
, Junit jupiter supports widening primitive conversion.
Also Junit supports string to following are the few target types for example.
-
boolean
/Boolean
– example ->"true"
→true
-
Enum
subclass – example ->"SECONDS"
→TimeUnit.SECONDS
-
java.time.LocalDate
– example ->"2017-03-14"
→LocalDate.of(2017, 3, 14)
-
java.util.Locale
– example ->"en"
→new Locale("en")
@ParameterizedTest @ValueSource(strings = {"2017-03-14", "2020-01-20"}) void testWith_implicit_conversion(LocalDate localDate) { System.out.println("localDate => "+localDate.toString()); assertTrue(localDate != null); }
Output :
localDate => 2017-03-14 localDate => 2020-01-20
3.2. Explicit conversion
Instead of relying on implicit conversion, we can create explicit converter specifying ArgumentConverter
and by using @ConvertWith
annotation like in the following example.
3.2.1 Create Argument Converter
public class DateFormatArgumentCoverter extends SimpleArgumentConverter { @Override protected Object convert(Object source, Class<?> targetType) { DateTimeFormatter format = DateTimeFormatter.ofPattern("MM-dd-yyyy"); return LocalDate.parse(source.toString(), format); } }
3.2.2 Explicit argument converter using @ConvertWith
@ParameterizedTest @ValueSource(strings = {"03-21-2017", "01-28-2020"}) void testWith_explicit_conversion( @ConvertWith(DateFormatArgumentCoverter.class) LocalDate localDate) { System.out.println("localDate => "+localDate.toString()); assertTrue(localDate != null); }
4. Argument Aggregation for Junit 5 Parameterized tests
4.1. ArgumentsAccessor
By default, each argument provided to a @ParameterizedTest
method corresponds to a single method parameter. ArgumentsAccessor
aggregates set of provided arguments and can be accessed those arguments in a type-safe manner with support for automatic type conversion.
@ParameterizedTest @CsvSource({ "Peter, admin, 1, 2017-03-14", "John, author, 2, 2017-03-14", "Martin, subscriber, 3, 2017-03-14" }) void testWith_ArgumentAccessor(ArgumentsAccessor accessor) { User user = new User(accessor.getString(0), accessor.getLong(2), accessor.getString(1), accessor.get(3, LocalDate.class)); System.out.println("name => "+user.getName()+ "; role => "+user.getRole()+ "; id => "+user.getId()); assertTrue(user.getName().length() >= 0); assertTrue(user.getId() >=1 && user.getId() <=3); assertTrue(!user.getRole().isEmpty()); }
Output :
name => Peter; role => admin; id => 1 name => John; role => author; id => 2 name => Martin; role => subscriber; id => 3
4.2. Custom Argument Aggregators
If you have large number of arguments, accessing arguments using ArgumentsAccessor
will become complex and messy. We can use custom aggregators that supported by Junit jupiter. To use a custom aggregator,
- Implement the
ArgumentsAggregator
interface . - Register it via the
@AggregateWith
annotation - Use it with
@ParameterizedTest
method.
Create Custom ArgumentAggregator
:
public class User { private String name; private long id; private String role; private LocalDate dateOfBirth; public User(String name, long id, String role, LocalDate dateOfBirth) { super(); this.name = name; this.id = id; this.role = role; this.dateOfBirth = dateOfBirth; } // Setters and getters }
UserAggregator.java
public class UserAggregator implements ArgumentsAggregator { @Override public User aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) { return new User(accessor.getString(0), accessor.getLong(2), accessor.getString(1), accessor.get(3, LocalDate.class)); } }
Use UserAggregator.java to a test case :
@ParameterizedTest @CsvSource({ "Peter, admin, 1, 2017-03-14", "John, author, 2, 2017-03-14", "Martin, subscriber, 3, 2017-03-14" }) void testWith_ArgumentAggregator(@AggregateWith(UserAggregator.class) User user) { System.out.println("name => "+user.getName()+ "; role => "+user.getRole()+ "; id => "+user.getId()+ "; dateOfBirth => "+user.getDateOfBirth().toString()); assertTrue(user.getName().length() >= 0); assertTrue(user.getId() >=1 && user.getId() <=3); assertTrue(!user.getRole().isEmpty()); }
Output :
name => Peter; role => admin; id => 1; dateOfBirth => 2017-03-14 name => John; role => author; id => 2; dateOfBirth => 2017-03-14 name => Martin; role => subscriber; id => 3; dateOfBirth => 2017-03-14
5. Conclusion
In this article we have seen what are the Junit 5 Parameterized tests, how to create parameterized test cases, several kind of argument sources for parameterized tests supported by Junit 5, argument conversion and arguments aggregators with several examples.
You can checkout source code from our github repository.
You also might be interested in following examples:
- Junit 5 Dynamic Tests
- Junit 5 tags and filter
- Junit 5 Timeout tests
- Junit 5 Repeated tests
- Junit 5 parallel execution
- Junit 5 – ParameterResolver
- Junit 5 @TestInstance
- Junit 5 + Gradle