Parameterized Test in Java
Parameterized tests in Java provide a powerful way to run the same test multiple times with different sets of input data. In order to go on with this topic on the new advanced features of JUnit, make sure you have already covered the principles of traditional unit tests and the JUnit framework and familiarized yourself with lifecycle annotations used to control the execution of tests.
Getting started
In this topic, we will expand our Calculator
class from the introductory JUnit topic by adding a new method that will calculate the maximum among two arguments:
public class Calculator {
public int maxOf(int a, int b) {
if (a >= b) {
return a;
} else {
return b;
}
}
}
We will also write the necessary tests to be sure that this method works correctly. We need to test three cases: when the first argument is greater than the second one, when the first argument is less than the second one, and when both arguments are equal.
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTests {
@Test
void testMaxFirstArgGreaterThanSecondArg() {
Calculator calculator = new Calculator();
int result = calculator.maxOf(2, 1);
int expected = 2;
assertEquals(expected, result);
}
@Test
void testMaxFirstArgLessThanSecondArg() {
Calculator calculator = new Calculator();
int result = calculator.maxOf(1, 2);
int expected = 2;
assertEquals(expected, result);
}
@Test
void testMaxFirstArgEqualToSecondArg() {
Calculator calculator = new Calculator();
int result = calculator.maxOf(2, 2);
int expected = 2;
assertEquals(expected, result);
}
}
Now, let's run these tests to be sure that our implementation of the max
method successfully passes all the tests. Running the test using Gradle gives the following output:
CalculatorTests > testMaxFirstArgEqualToSecondArg() PASSED
CalculatorTests > testMaxFirstArgLessThanSecondArg() PASSED
CalculatorTests > testMaxFirstArgGreaterThanSecondArg() PASSED
However, if you look at these tests you will notice that they are nearly identical and the only difference is the values we use in their bodies. Do we have a way to write such tests in a cleaner manner? JUnit provides us such an option, which is called "parameterized tests".
First, let's add the following dependency to our project so that JUnit be able to work with parameterized tests.
Gradle:
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-params:5.7.1"
}
Maven:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.7.1</version>
<scope>test</scope>
</dependency>
If you use JUnit Jupiter aggregator artifact dependency 'org.junit.jupiter:junit-jupiter:5.7.1'
, it automatically pulls in dependencies on junit-jupiter-api
, junit-jupiter-params
, and junit-jupiter-engine
.
@ParameterizedTest
@ParameterizedTest
allows us to invoke a single test method multiple times, passing different arguments to it. Take a look at the following code snippet:
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTests {
@ParameterizedTest
@CsvSource({"2, 1, 2", "1, 2, 2", "1, 1, 1"})
void testMax(int first, int second, int expected) {
Calculator calculator = new Calculator();
int result = calculator.maxOf(first, second);
assertEquals(expected, result);
}
}
In this example, we use the @ParameterizedTest
annotation instead of @Test
to specify that the corresponding test should be executed multiple times with different arguments. We also use the @CsvSource
annotation to provide an array of such arguments. JUnit has plenty of annotations for different sources of arguments, such as @ValueSource
, @EnumSource
, @MethodSource
, @CsvSource
, @CsvFileSource
, and @ArgumentsSource
, some of which we are going to discuss below.
Note that the test method now has three parameters: int first
, int second
, and int expected
, which are used in the body of the test method, and respective arguments are supplied by JUnit at runtime based on the specified argument source. Let's run this test:
CalculatorTests > [1] 2, 1, 2 PASSED
CalculatorTests > [2] 1, 2, 2 PASSED
CalculatorTests > [3] 1, 1, 1 PASSED
The default output consists of the current invocation index and the list of the arguments. We can specify a custom message format for a test using attributes and placeholders, for example:
@ParameterizedTest(name = "{index} => maxOf({0}, {1}) == {2}")
The execution of the same test using the custom message format gives the following output:
CalculatorTests > 1 => maxOf(2, 1) == 2 PASSED
CalculatorTests > 2 => maxOf(1, 2) == 2 PASSED
CalculatorTests > 3 => maxOf(1, 1) == 1 PASSED
With the help of custom display names, you can easily and conveniently provide pretty and informative test outputs.
Sources of arguments
JUnit provides a number of annotations to define a source of arguments. Such arguments may be a sequence of test method arguments of the same type. They accept a single test method argument or a sequence of arguments of the same or different types, which in turn accept multiple arguments. We are going to discuss @ValueSource
, @CsvSource
, and @CsvFileSource
in detail and have a glimpse of some other annotations. You can find the full information about them in the official guide.
@ValueSource
@ValueSource
is an argument source that supplies an array of literal values for test methods with a single parameter. Such literal values may be of any of the following types: short
, byte
, int
, long
, float
, double
, char
, boolean
, java.lang.String
, and java.lang.Class
.
Let us add another method to our Calculator
class, which will accept a single int
argument and return boolean
:
public boolean isEven(int a) {
return a % 2 == 0;
}
After that, we will use the following test method that will be invoked multiple times with different integer arguments supplied by @ValueSource
:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTests {
@ParameterizedTest
@ValueSource(ints = { 0, 2, 4, 1000 })
void testIsEven(int arg) {
assertTrue(new Calculator().isEven(arg));
}
}
For non-primitive types, you can use @EmptySource
, @NullSource
or @NullAndEmptySource
annotations to pass null
and empty values. In order to avoid writing too much code, we will be using more abstract examples to illustrate how to pass different types of arguments to test methods. In the following code snippets, an empty arg
array is passed to the testMethod(int[] arg)
method, and the testNullAndEmpty(List<String> arg)
method is invoked twice: the first time arg
is null, and the second time arg
is empty.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTests {
@ParameterizedTest
@EmptySource
void testEmpty(int[] arg) {
assertEquals(0, arg.length);
}
@ParameterizedTest
@NullAndEmptySource
void testNullAndEmpty(List<String> arg) {
assertTrue(arg == null || arg.isEmpty());
}
}
You can even combine these annotations with @ValueSource
values to check the whole range of test cases in a single test method.
@MethodSource
This annotation allows you to use a method of your test class or an external class as a source of arguments. Each method annotated with @MethodSource
must satisfy the following requirements: it must be static
, it must not accept any arguments, and must return a stream, an array, or a collection of arguments.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTests {
@ParameterizedTest
@MethodSource("stringFactory")
void testStrings(String str) {
assertFalse(str.isEmpty());
}
static List<String> stringFactory() {
return List.of("apple", "banana", "lemon", "orange");
}
}
You may use non-static methods as long as they are internal methods of the test class annotated with @TestInstance(Lifecycle.PER_CLASS)
, but if you use methods of external classes as a source of arguments, they must always be declared as static
.
If a parameterized test method has multiple parameters, your argument source method needs to return a collection, a stream, or an array of Arguments
or an array of Object
. In this case, Arguments
can be generated by the arguments
static method:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.params.provider.Arguments.arguments;
class CalculatorTests {
@ParameterizedTest
@MethodSource("argFactory")
void testStringLength(String str, int length) {
assertEquals(length, str.length());
}
static List<Arguments> argFactory() {
return List.of(arguments("apple", 5), arguments("watermelon", 10));
}
}
See the official JUnit documentation for more detailed information on @MethodSource
.
Working with CSV
In the first example of a parameterized test, we used the annotation @CsvSource
. It allows you to supply a list of arguments as comma-separated values (CSV format), for example:
@CsvSource({ "apple, 5", "strawberry, 10", "cherry, 6" })
In this case, each value is represented by a String
literal containing a list of arguments separated by a comma which serves as the default delimiter. @CsvSource
also has a number of attributes to define the format of the arguments. You can change the default delimiter to another character or even a String
literal, as well as define the representation of empty and null
values. However, all these attributes are optional and can be used when needed.
Due to its flexibility, @CsvSource
is well suited for supplying arguments for methods with multiple input parameters of different types.
In addition to @CsvSource
, JUnit has the @CsvFileSource
annotation which is used to load a CSV file from the class path or the local file system. Each line from a CSV file serves as a source of arguments for one invocation of the parameterized test. You may skip the desired number of lines in the file by setting the numLinesToSkip
attribute. Also, if you want any lines in the CSV file to be ignored, you can use the symbol #
at the beginning of the respective lines to comment them out.
Here is an example of a CSV file:
String, Length
apple, 5
strawberry, 10
# commented line
cherry, 6
And an example of the @CsvFileSource
annotation:
@CsvFileSource(resources = "/dataset.csv", numLinesToSkip = 1)
This way you can use big sets of input data for your tests. If you are interested in detailed instructions on how to work with CSV argument sources, check the corresponding sections of the official JUnit5 guide.
Conclusion
In this topic, you learned about the concept of parameterized tests and familiarized yourself with the tools JUnit provides for this purpose. Parameterized tests are a convenient tool for writing effective and concise tests. Instead of multiple test methods, you can have one method denoted by @ParameterizedTests
that takes parameters so that you can supply different arguments to it. This allows you to reuse your code efficiently and improve the readability of your tests.
In addition to this, JUnit has many options to set up argument sources for your tests, including @ValueSource
, @MethodSource
, @CsvSource
and a number of other annotations. By combining different sources, you can use sets of input data of any size to cover as many test cases as possible. This allows you to test units of code with very complex logic and extremely large numbers of execution paths that otherwise could not be reliably tested.