Skip to content
Chapter 08 · JUnit 5 & Mockito

Parameterized Tests Part 1: Single and Multiple Values ​

@RepeatedTest runs a test several times with the same input. A parameterized test runs the same test several times with different inputs, which is far more common in real projects.

java
@ParameterizedTest
@ValueSource(strings = {"civic", "level", "madam"})
void testPalindrome(String word) {
    assertTrue(MyStringUtil.isPalindrome(word));
}

@ParameterizedTest marks the method (instead of @Test); a source annotation feeds the inputs. The test runs three times: word is "civic", then "level", then "madam".

How you feed the data depends on how many parameters the test takes:

Test parametersSources
one@ValueSource, @EnumSource (plus @NullSource, @EmptySource)
several@CsvSource, @CsvFileSource
objects and more complex casescovered in part 2

One parameter ​

@ValueSource ​

Feeds a single parameter. It supports literal values of these types: strings, ints, shorts, bytes, longs, floats, doubles, chars, booleans and classes. Not arbitrary objects.

Implicit conversion ​

JUnit converts between compatible types automatically:

java
@ParameterizedTest
@ValueSource(strings = {"1", "2", "hello"})
void testInteger(int number) { ... }

"1" and "2" become the ints 1 and 2. "hello" cannot:

text
[3] hello   ✘ ParameterResolutionException: Error converting parameter at index 0:
              Failed to convert String "hello" to type int

Implicit conversion is handy, but do not rely on it for anything non-trivial; convert explicitly.

Explicit conversion: @ConvertWith ​

java
@ParameterizedTest
@ValueSource(strings = {"hello", "world"})
void testUppercase(@ConvertWith(StringUppercaseConverter.class) String word) {
    // word is "HELLO", then "WORLD"
}
java
public class StringUppercaseConverter implements ArgumentConverter {

    @Override
    public Object convert(Object source, ParameterContext context) {
        if (source instanceof String s && context.getParameter().getType() == String.class) {
            return s.toUpperCase();
        }
        return source;
    }
}

The sequence:

  1. @ValueSource supplies the raw value ("hello").
  2. @ConvertWith calls your converter before the value is injected.
  3. The converted value is injected into the parameter.
  4. The test runs.

source is the raw value; the ParameterContext describes the parameter: its type, its method, its index in the parameter list (more on contexts in the extensions chapter). An ArgumentConverter converts one parameter; put @ConvertWith on each parameter that needs it, which works just as well in tests with several parameters. To build one object from several values, use an aggregator (part 2).

null and empty values ​

@ValueSource cannot supply null. Dedicated sources do:

  • @NullSource: one run with null
  • @EmptySource: one run with an empty value ("", an empty list, ...)
  • @NullAndEmptySource: both

They combine with other sources:

java
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"civic", "level", "madam"})
void testPalindrome(String word) {
    assertTrue(MyStringUtil.isPalindrome(word));
}

Five runs: null, "", "civic", "level", "madam". (null cannot go into a primitive parameter: "Cannot convert null to primitive value of type int".)

@EnumSource ​

java
@ParameterizedTest
@EnumSource(Day.class)
void testDay(Day day) { ... }

Runs once per constant: seven times for a Day enum. To pick constants:

java
@ParameterizedTest
@EnumSource(value = Day.class, names = {"MONDAY", "TUESDAY"}, mode = EnumSource.Mode.INCLUDE)
void testWeekStart(Day day) { ... }   // MONDAY, TUESDAY

names must match the constants exactly. mode = INCLUDE is the default; mode = EXCLUDE runs every constant except the named ones.

Several parameters ​

A typical case: creating a user needs a country, a currency and a status, and you want to test createUser with India / INR / ACTIVE, Russia / RUB / INACTIVE, and so on.

@CsvSource ​

Inline CSV: each string is one run, its values separated by the delimiter (a comma by default) and bound to the parameters in order.

java
@ParameterizedTest
@CsvSource({"1, 2, 3", "5, 5, 10"})
void testAdd(int a, int b, int expected) {
    assertEquals(expected, a + b);
}

Run 1: a=1, b=2, expected=3. Run 2: a=5, b=5, expected=10.

A different delimiter and a header line:

java
@ParameterizedTest
@CsvSource(delimiter = '|', useHeadersInDisplayName = true, value = {
        "value1 | value2 | expected",
        "1      | 2      | 3",
        "5      | 5      | 10"
})
void testAdd(int a, int b, int expected) { ... }

useHeadersInDisplayName = true (default false) treats the first line as a header: it is not converted (otherwise "value1" would fail to become an int) and is used in the run names.

Commas inside a value: wrap it in single quotes:

java
@ParameterizedTest
@CsvSource({"'Delhi, India', 12"})
void testLength(String word, int length) { ... }   // word = "Delhi, India"

Empty and null values:

java
@CsvSource({"hello, ''"})    // second argument: "" (empty string)
@CsvSource({"hello,"})       // second argument: null (nothing at all)

@CsvSource(value = {"hello, N/A"}, nullValues = "N/A")   // "N/A" becomes null

Whitespace: leading and trailing spaces are trimmed by default. To keep them:

java
@CsvSource(value = {" hello , world "}, ignoreLeadingAndTrailingWhitespace = false)
// " hello " and " world "

When you set more than one attribute, the values go in value = {...}; with only values, value = can be left out.

@CsvFileSource ​

For a large data set, a thousand inline strings are unreadable. Load the CSV from a file instead:

text
src/test/resources/input-data.csv

value1,value2,expected
1,2,3
5,5,10
java
@ParameterizedTest
@CsvFileSource(resources = "/input-data.csv", numLinesToSkip = 1)
void testAdd(int a, int b, int expected) { ... }

resources is a classpath path, so the file goes under src/test/resources. numLinesToSkip = 1 skips the header (default 0). Alternatively, useHeadersInDisplayName = true treats the first line as the header.

Use one or the other, not both. With both, the first line is skipped and the next line is then taken as the header: two lines are lost, and only 5,5,10 runs.

Next: Part 2 feeds objects and more complex data: @MethodSource, @ArgumentsSource, ArgumentsAccessor, @AggregateWith, and custom display names.

Check yourself ​

  1. What is the difference between @RepeatedTest and @ParameterizedTest?
  2. Which sources feed one parameter, and which several?
  3. @ValueSource(strings = {"1", "hello"}) feeds an int parameter. What happens?
  4. In what order do the source, the converter and the test run with @ConvertWith?
  5. Can @ConvertWith be used in a test with three parameters?
  6. How do you run a test with null and "" in addition to other strings?
  7. How do you run an enum test for every day except Monday and Tuesday?
  8. How do you pass Delhi, India as one value in @CsvSource?
  9. In @CsvSource, what do '' and an empty slot produce?
  10. Why should you not combine numLinesToSkip = 1 with useHeadersInDisplayName = true?
Answers
  1. Repeated: same input, several times. Parameterized: same test, different inputs.
  2. One: @ValueSource, @EnumSource (and @NullSource/@EmptySource). Several: @CsvSource, @CsvFileSource.
  3. "1" is converted to 1 and passes; "hello" fails with "Failed to convert String "hello" to type int".
  4. The source supplies the raw value, the converter converts it, the value is injected, then the test runs.
  5. Yes, on any parameter that needs conversion.
  6. Add @NullAndEmptySource alongside @ValueSource.
  7. @EnumSource(value = Day.class, names = {"MONDAY", "TUESDAY"}, mode = EnumSource.Mode.EXCLUDE).
  8. Wrap it in single quotes: 'Delhi, India'.
  9. '' gives an empty string; an empty slot gives null.
  10. The first line is skipped and the second is treated as the header, so two lines are lost.