Parameterized tests
Often one check has to be repeated for many inputs: a slug for ten different titles, a rejection for an empty, blank and null title. Copying the test ten times is a bad idea. A parameterized test is written once, receives the data as method parameters, and JUnit runs it once for every set of values.
The class under test
A slug is the part of a post URL built from its title: Hello, World! becomes hello-world. Create src/main/java/com/example/blog/Slugs.java:
// src/main/java/com/example/blog/Slugs.java
package com.example.blog;
import java.util.Locale;
public final class Slugs {
private Slugs() {
}
public static String of(String title) {
return title.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-|-$", "");
}
}Everything except Latin letters and digits turns into a hyphen; hyphens at the edges are removed.
@ValueSource
Instead of @Test you write @ParameterizedTest and add a data source. The simplest one is @ValueSource — an array of values of one type:
// src/test/java/com/example/blog/SlugsTest.java
@ParameterizedTest
@ValueSource(strings = {"Hello", "Hello, World!", " Java 25 "})
void slugHasNoSpaces(String title) {
assertFalse(Slugs.of(title).contains(" "));
}The test runs three times, with a different title each time. Besides strings there are ints, longs, doubles and other arrays. The annotations are imported from org.junit.jupiter.params and org.junit.jupiter.params.provider — the junit-jupiter dependency already contains them.
@CsvSource
When the test needs several parameters — input and expected result — use @CsvSource. Each string is one run, values are separated by commas:
// src/test/java/com/example/blog/SlugsTest.java
@ParameterizedTest
@CsvSource({
"Hello, hello",
"'Hello, World!', hello-world",
"' Java 25 ', java-25",
"JUnit & Mockito, junit-mockito"
})
void slugFromTitle(String title, String expected) {
assertEquals(expected, Slugs.of(title));
}Spaces around values are trimmed, so you can align columns. A value with a comma or with significant spaces is wrapped in single quotes. JUnit converts strings into the parameter types itself: int, long, boolean, BigDecimal, enums.
Empty values and null
@NullAndEmptySource adds two runs: with null and with an empty string. Combined with @ValueSource you get all the "nothing" cases at once:
// src/test/java/com/example/blog/SlugsTest.java
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t"})
void blankTitleIsRejected(String title) {
PostService service = new PostService(new InMemoryPostRepository(), (to, message) -> { });
assertThrows(IllegalArgumentException.class, () -> service.create(title, "anna"));
}Four runs: null, "", a space and a tab. All three tests of the class give Tests run: 11 in the Maven report.
The names of runs
The IDE shows each run separately. JUnit 6 puts string values in quotes, so empty and blank strings are visible: the runs of blankTitleIsRejected are called [1] null, [2] "", [3] " " and [4] "\t". You can set your own format with name:
// src/test/java/com/example/blog/SlugsTest.java
@ParameterizedTest(name = "slug of {0} is {1}"){0}, {1} are the parameters, {index} is the run number. The runs are now called slug of "Hello" is "hello" and so on. Maven reports name the runs by the method and index: slugFromTitle(String, String)[2].
Common mistake
If you forget the quotes around a value with a comma, the string splits into three columns. JUnit 6 does not complain about the extra column, it just takes the first two:
"Hello, World!, hello-world"[ERROR] com.example.blog.CsvFailTest.slugFromTitle(String, String)[2] -- Time elapsed: 0.015 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <World!> but was: <hello>The title became Hello and the expected value became World!. Values with commas always go in single quotes: 'Hello, World!'.