Better unit test with JUnit 5 Parameterized Tests
In many situations, you have to write unit tests that are identical, with the exception of the input or possibly the expected value. It is tempting to write the same test multiple times or to write one test with the dynamic part in a loop. JUnit 5, however, offers a better option with the @ParametrizedTest annotation that allows you to have one test, but with different inputs and expected values. Let’s take a look.
Importing the library into your project
To be able to use the parameterized test, you will need to include it in your project. I will be using Gradle for dependency management.
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
}
Now, you can start writing your unit tests and you will be able to use the @ParameterizedTest annotation.
Single Parameter with @ValueSource
Let’s start with a simple scenario. You have a method called hasDigits(String str) that takes a String variable and returns if that string contains at least one digit. We want to write different unit tests for it to validate that it works properly. To do this, we can use the @ValueSoruce annotation to send different parameters to the unit test.
@ParameterizedTest
@ValueSource(strings = {
"test1",
" 1 ",
"1111",
"1"
})
void testValidateHasDigits(String param) {
Assertions.assertTrue(hasDigits(param));
}
Now, with one single test we can validate all the strings we want without the need to re-write it several times. If we later discover a new test scenario, we can just add it to the strings list in the annotation. If you want a different type, fear not. The annotation accepts parameters of most primitives (ints, booleans, chars, etc.). Here is an example:
@ParameterizedTest
@ValueSource(ints = {2, 3, 5, 13})
void testValidateIsPrime(int param) {
Assertions.assertTrue(isPrime(param));
}
Null and empty String
The previous methods do not allow adding null or empty strings. Fear not though, as JUnit 5 has a workaround for this by providing three annotations that will inject test cases for null or empty values alongside the values you added in the strings argument.
@NullSource // Includes a test where the parameter is null
@EmptySoruce // Includes a test where the parameter is an empty String
@NullAndEmptySoruce // Includes two tests, equivalent to having both annotations from above
Multiple parameters using @CsvSource
The above scenarios are nice, but for most unit tests you have more than one parameter. Most probably you have one or more inputs and expected output. Thankfully, JUnit 5 can handle this scenario as well by providing the @CsvSource annotation. This will allow us to have multiple parameters provided to our method.
For this example, we want to test a method similar to the hasDigits() one from above, however, this one returns the number of digits. We can use the @CsvSource to provide both the input and the expected result:
@ParameterizedTest
@CsvSource(value = {
"test,0",
"test1test,1",
"123,3"
})
void testCountDigits(String str, int expectedNoDigits) {
Assertions.assertEquals(expectedNoDigits, countDigits(str));
}
As you can see, JUnit knows how to properly parse the CSV provided and can even interpret the int variable properly.
If your String contains ‘,’ and parsing does not work properly, you can specify the delimiter for the CSV source:
@ParameterizedTest
@CsvSource(value = {
"test;0",
"test1test;1",
"123;3"
}, delimiter = ';')
void testCountDigits(String str, int expectedNoDigits) {
Assertions.assertEquals(expectedNoDigits, countDigits(str));
}
If you want to test with special characters (like \n), you can include your text in quotes. By default, the quote character is ‘, however it too can be customized if needed using quoteCharacter.
@ParameterizedTest
@CsvSource(value = {
"\'123\na3a\';4"
}, delimiter = ';')
void testCountDigits(String str, int expectedNoDigits) {
Assertions.assertEquals(expectedNoDigits, countDigits(str));
}
Reading from CSV files for test input
Working with CSV saves a lot of time, however, your test inputs can become really big and hard to maintain, especially if you have many parameters. That is why JUnit allows us to specify a CSV file as the input. The file must be in the classpath of the application for this to work.
@ParameterizedTest
@CsvFileSource(resources = "/data.csv")
void testCountDigits(String str, int expectedNoDigits) {
Assertions.assertEquals(expectedNoDigits, countDigits(str));
}
If your file has a header, and many CSV files have one, you can tell JUnit to skip the first line (or how many you want) by using numLinesToSkip = 1 in the annotation. Also, other configuration options that you are already familiar with from the CsvSource are present here as well.
Conclusions
JUnit 5 offers all the tools needed to write elegant unit tests. Parameterized tests are easier to maintain, take up less code and don’t require a complex setup. There are even more features, like having Enums as parameters or actual methods that return the data. There are many possibilities and most probably there is a feature that matches your scenario.