JUnit 5 Parameterized Tests: @ParameterizedTest With Examples

Write JUnit 5 parameterized tests with @ValueSource, @CsvSource, @MethodSource, @FieldSource and @ParameterizedClass, display names and JUnit 6 notes.

Three-step flow. Step 1 lists the sources @ValueSource, @NullAndEmptySource, @EnumSource, @CsvSource, @CsvFileSource, @MethodSource, @FieldSource and @ArgumentsSource, all pointing to step 2, a Stream of Arguments with one entry per invocation. Step 3 shows per invocation the conversion of a string to LocalDate, aggregation with ArgumentsAccessor or @AggregateWith, the display name such as [1] "10001", and the test method run with @BeforeEach and @AfterEach.

A JUnit 5 parameterized test is a test method annotated with @ParameterizedTest that JUnit runs once for every set of arguments from a source annotation such as @ValueSource, @CsvSource or @MethodSource. Each invocation appears in the report as its own test, with its own display name and result.

A parameterized test fits a rule that must hold for many inputs, for example a price calculation for every membership tier, a validator for valid and invalid ZIP codes, or a fee table with boundary values. One method replaces a dozen copies of the same test that differ only in their data.

The following example checks the discount of three membership tiers with one test method.

@ParameterizedTest(name = "{2}: {1} x {0} = {3}", quoteTextArguments = false)
@CsvSource({
    "10.00, 2, BASIC,  20.00",
    "10.00, 2, SILVER, 19.00",
    "10.00, 2, GOLD,   18.00"
})
void appliesTierDiscount(BigDecimal unitPrice, int quantity, MembershipTier tier, BigDecimal expected) {
    assertEquals(expected, calculator.finalPrice(unitPrice, quantity, tier));
}
BASIC: 2 x 10.00 = 20.00
SILVER: 2 x 10.00 = 19.00
GOLD: 2 x 10.00 = 18.00

Notice that JUnit converts the CSV text into BigDecimal, int and the enum MembershipTier before it calls the method, and that the name pattern builds a readable display name from the arguments.

We start with the setup and the simple sources, move on to CSV data, factory methods and fields, and after that cover argument conversion, aggregation and display names. The last sections show parameterized classes, which JUnit added in 5.13, and the configuration errors that new users run into.

1. Setup and the JUnit 6 Changes

Parameterized tests are in the module junit-jupiter-params. The aggregate artifact junit-jupiter already includes it, so a project with the usual JUnit Maven dependency needs nothing else. Our project uses Java 25, JUnit 6.1.3 and Maven Surefire 3.6.0.

<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>

The annotations are the same in JUnit 5 and JUnit 6, so the examples run on JUnit 5.13 as well, except the attribute quoteTextArguments, which JUnit 6.0 added. JUnit 6.0 also changed a few details of display names and CSV parsing.

  • Text arguments are quoted in display names by default, so a test shows [1] “10001” instead of [1] 10001. The attribute quoteTextArguments = false turns it off.
  • @CsvSource and @CsvFileSource parse CSV with the FastCSV library. Malformed input fails with different messages, extra characters after a closing quote are no longer allowed, and the lineSeparator attribute of @CsvFileSource was removed.
  • An ArgumentsProvider implements provideArguments(ParameterDeclarations, ExtensionContext). The old method with only ExtensionContext is deprecated since 5.13.

The JUnit tutorial lists the other topics of this series, and the JUnit User Guide documents every source attribute.

2. How Parameterized Tests Work

A @ParameterizedTest method is a test template. JUnit asks every source annotation on the method for its arguments, combines them into one stream and invokes the method once per entry. Before each invocation, JUnit converts the arguments to the parameter types, builds the display name and runs the full lifecycle with @BeforeEach and @AfterEach.

Three-step flow. Step 1 lists the sources @ValueSource, @NullAndEmptySource, @EnumSource, @CsvSource, @CsvFileSource, @MethodSource, @FieldSource and @ArgumentsSource, all pointing to step 2, a Stream of Arguments with one entry per invocation. Step 3 shows per invocation the conversion of a string to LocalDate, aggregation with ArgumentsAccessor or @AggregateWith, the display name such as [1] "10001", and the test method run with @BeforeEach and @AfterEach.
Every source contributes argument sets, and JUnit runs the method once per set with conversion, display name and lifecycle.

The rules for the method are short. It must not be private or static, it returns void, and it needs at least one source. The parameters that the source fills come first in the parameter list, followed by aggregators and by parameters from other resolvers such as TestInfo.

3. Simple Values With @ValueSource

The annotation @ValueSource supplies one literal per invocation. It has one array attribute per type, namely shorts, bytes, ints, longs, floats, doubles, chars, booleans, strings and classes, and we set only one of them.

@ParameterizedTest
@ValueSource(strings = {"10001", "94105", "60614"})
void acceptsFiveDigits(String zip) {
    assertTrue(validator.isValid(zip));
}
[1] "10001"
[2] "94105"
[3] "60614"

A shipping fee table needs boundary values, and doubles supplies them. The same pattern tests that invalid weights throw.

@ParameterizedTest
@ValueSource(doubles = {0.2, 0.5, 1.0})
void chargesLowestFeeUpToOneKilogram(double weight) {
    assertEquals(new BigDecimal("3.99"), shippingFee.forWeight(weight));
}

@ParameterizedTest
@ValueSource(doubles = {0, -1.5})
void rejectsZeroOrNegativeWeight(double weight) {
    assertThrows(IllegalArgumentException.class, () -> shippingFee.forWeight(weight));
}

An annotation attribute cannot be null in Java, so @ValueSource can never supply a null argument. The next section closes that gap.

4. Null and Empty Values

Input validation must reject null and empty input as well as malformed text. Three annotations add these cases to a parameterized test.

  • @NullSource supplies a single null. It cannot be used with a primitive parameter.
  • @EmptySource supplies a single empty value for String, collections such as List, Set and Map, and arrays. JUnit 6.1 added Iterable and Iterator types.
  • @NullAndEmptySource combines both.
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "1234", "123456", "abcde"})
void rejectsInvalidInput(String zip) {
    assertFalse(validator.isValid(zip));
}
[1] null
[2] ""
[3] " "
[4] "1234"
[5] "123456"
[6] "abcde"

We can see that the sources add up. The method runs 6 times, first with the null and the empty string, followed by the four values of @ValueSource. The quoting in JUnit 6 makes the blank string ” “ visible in the report, where JUnit 5 showed an empty-looking name.

5. Enum Constants With @EnumSource

The annotation @EnumSource runs the test once per enum constant. When the parameter type is the enum, the class attribute can be left out. The attributes names and mode select a subset, and the mode EXCLUDE inverts the selection.

@ParameterizedTest
@EnumSource(MembershipTier.class)
void discountIsNeverAboveTenPercent(MembershipTier tier) {
    assertTrue(tier.discountPercent() <= 10);
}

@ParameterizedTest
@EnumSource(names = {"SILVER", "GOLD"})
void paidTiersGetADiscount(MembershipTier tier) {
    assertTrue(tier.discountPercent() > 0);
}

@ParameterizedTest
@EnumSource(mode = EnumSource.Mode.EXCLUDE, names = "BASIC")
void allTiersExceptBasicGetADiscount(MembershipTier tier) {
    assertTrue(tier.discountPercent() > 0);
}

The first test runs for BASIC, SILVER and GOLD, and the other two run for SILVER and GOLD. When the business adds a PLATINUM tier, the first test covers it without any change, which is the main reason to prefer @EnumSource over a list of strings. Other modes such as MATCH_ANY select constants by regular expression.

6. Several Arguments With @CsvSource

The annotation @CsvSource supplies several arguments per invocation as comma-separated values. Each string is one row, and each column goes to the parameter at the same position. By default, JUnit strips the leading and trailing whitespace of unquoted values, so columns can be aligned.

A value that contains the delimiter goes in single quotes, the default quoteCharacter. An empty quoted value ” becomes an empty string, and an empty unquoted value becomes null.

@ParameterizedTest(name = "{0}")
@CsvSource({
    "'Dune, Messiah', Frank Herbert, 1969",
    "Emma,           Jane Austen,   1815"
})
void readsQuotedValues(String title, String author, int year) {
    Book book = new Book(title, author, LocalDate.of(year, 1, 1));
    assertEquals(year, book.published().getYear());
}
"Dune, Messiah"
"Emma"

For longer tables, the attribute textBlock takes a Java text block. In a text block, a line that starts with # is a comment. The attribute useHeadersInDisplayName reads the first row as headers and puts them into the display names, and nullValues turns a marker such as N/A into null.

@ParameterizedTest
@CsvSource(useHeadersInDisplayName = true, nullValues = "N/A", textBlock = """
    title,           author,        published
    Dune,            Frank Herbert, 1965-08-01
    # a book without a known author
    Beowulf,         N/A,           1000-01-01
    """)
void readsTextBlock(String title, String author, LocalDate published) {
    Book book = new Book(title, author, published);
    if (title.equals("Beowulf")) {
        assertNull(book.author());
    } else {
        assertEquals("Frank Herbert", book.author());
    }
}
[1] title = "Dune", author = "Frank Herbert", published = "1965-08-01"
[2] title = "Beowulf", author = null, published = "1000-01-01"

Other attributes change the format. The attribute delimiter or delimiterString replaces the comma, emptyValue replaces the empty string for ”, and commentCharacter (since JUnit 6.0.1) replaces # in text blocks.

7. Reading Test Data From a File With @CsvFileSource

When the test data grows to dozens of rows or comes from the business team, we keep it in a CSV file. The annotation @CsvFileSource reads it from the classpath with resources or from the file system with files. The attribute numLinesToSkip skips the header row, and lines starting with # are comments.

weight,fee
0.5,3.99
1.0,3.99
# parcels between 1 and 5 kg
2.5,7.99
5.0,7.99
12,14.99
@ParameterizedTest(name = "{0} kg -> {1}")
@CsvFileSource(resources = "/shipping-fees.csv", numLinesToSkip = 1)
void chargesFeeFromTable(double weight, BigDecimal expectedFee) {
    assertEquals(expectedFee, shippingFee.forWeight(weight));
}
"0.5" kg -> "3.99"
"1.0" kg -> "3.99"
"2.5" kg -> "7.99"
"5.0" kg -> "7.99"
"12" kg -> "14.99"

The file is in src/test/resources/shipping-fees.csv, and the leading slash makes the path absolute on the classpath. Notice that the display name quotes the numbers. JUnit builds the name from the original CSV text before converting it, so every CSV value counts as text.

8. Objects From a Factory Method With @MethodSource

Annotations accept only constants, so we need code to pass objects such as BigDecimal, records or lists. The annotation @MethodSource names a factory method that returns a Stream, a Collection, an Iterable, an Iterator or an array of arguments. For several parameters, each element is an Arguments object.

static Stream<Arguments> smallOrders() {
    return Stream.of(
            arguments(new BigDecimal("12.50"), 1, MembershipTier.BASIC, new BigDecimal("12.50")),
            arguments(new BigDecimal("12.50"), 3, MembershipTier.SILVER, new BigDecimal("35.63")));
}

@ParameterizedTest
@MethodSource("smallOrders")
void pricesSmallOrders(BigDecimal unitPrice, int quantity, MembershipTier tier, BigDecimal expected) {
    assertEquals(expected, calculator.finalPrice(unitPrice, quantity, tier));
}

A factory method in the test class must be static, unless the class uses @TestInstance(Lifecycle.PER_CLASS). Without a name, @MethodSource looks for a factory method with the same name as the test method.

static Stream<Integer> rejectsInvalidQuantity() {
    return Stream.of(0, -1);
}

@ParameterizedTest
@MethodSource
void rejectsInvalidQuantity(int quantity) {
    assertThrows(IllegalArgumentException.class,
            () -> calculator.finalPrice(BigDecimal.TEN, quantity, MembershipTier.BASIC));
}

Test data that several test classes share goes into its own class, and @MethodSource refers to it by fully qualified name and #. The factory largeOrders() also uses argumentSet() (since JUnit 5.11), which gives an argument set a name for the report.

public static Stream<Arguments> largeOrders() {
    return Stream.of(
            arguments(new BigDecimal("4.50"), 100, MembershipTier.GOLD, new BigDecimal("405.00")),
            argumentSet("bulk order of a cheap item", new BigDecimal("0.99"), 1000, MembershipTier.BASIC,
                    new BigDecimal("990.00")));
}
@ParameterizedTest
@MethodSource("com.howtodoinjava.junit.params.OrderData#largeOrders")
void pricesLargeOrders(BigDecimal unitPrice, int quantity, MembershipTier tier, BigDecimal expected) {
    assertEquals(expected, calculator.finalPrice(unitPrice, quantity, tier));
}
[1] 4.50, 100, GOLD, 405.00
[2] bulk order of a cheap item

9. Arguments From a Field With @FieldSource

The annotation @FieldSource (since JUnit 5.11) reads the arguments from a field instead of a method. The field holds a Collection, an Iterable, an array or a Supplier of a stream. A constant list of sample data is the typical case.

static final List<String> SAMPLE_ZIPS = List.of("02134", "73301", "99501");

@ParameterizedTest
@FieldSource("SAMPLE_ZIPS")
void acceptsSampleZips(String zip) {
    assertTrue(validator.isValid(zip));
}

A field of type Stream is not allowed, because a stream can be consumed only once and the field may be read for several tests. When the data must be a stream, the field type is Supplier<Stream<Arguments>>. The same static rule as for factory methods applies to fields in the test class.

10. A Reusable Source With @ArgumentsSource

When several test classes need the same computed data, we implement ArgumentsProvider and register it with @ArgumentsSource. For example, a bookstore marks books from the last three months as new releases, and the provider creates such books relative to a fixed date.

public class NewReleaseArgumentsProvider implements ArgumentsProvider {

    static final LocalDate TODAY = LocalDate.of(2026, 10, 11);

    @Override
    public Stream<? extends Arguments> provideArguments(ParameterDeclarations parameters,
                                                        ExtensionContext context) {
        return Stream.of(
                Arguments.of(new Book("The Last Shift", "A. Rao", TODAY.minusDays(10))),
                Arguments.of(new Book("Night Train", "M. Weber", TODAY.minusMonths(2))));
    }
}
@ParameterizedTest
@ArgumentsSource(NewReleaseArgumentsProvider.class)
void detectsNewReleases(Book book) {
    assertTrue(book.isNewRelease(NewReleaseArgumentsProvider.TODAY));
}
[1] Book[title=The Last Shift, author=A. Rao, published=2026-10-01]
[2] Book[title=Night Train, author=M. Weber, published=2026-08-11]

The first parameter, ParameterDeclarations, describes the parameters of the test method, so a provider can adapt its output to them. A provider with only the old provideArguments(ExtensionContext) method still compiles, but that method is deprecated, and new code implements the two-parameter version.

11. Argument Conversion and Aggregation

JUnit converts each argument to the parameter type. Implicit conversion handles primitives and their wrappers, enums, BigDecimal, the java.time types such as LocalDate, File, Path, UUID and more. For other types, a fallback uses a static factory method or a constructor that takes a single String. An ArgumentConverter with @ConvertWith covers everything else.

When a CSV row describes one object, a parameter for every column makes the method signature long. An ArgumentsAccessor parameter receives all columns of the row and reads them by index, with conversion.

@ParameterizedTest
@CsvSource({
    "Dune, Frank Herbert, 1965-08-01",
    "Emma, Jane Austen,   1815-12-23"
})
void readsWithAccessor(ArgumentsAccessor arguments) {
    Book book = new Book(arguments.getString(0), arguments.getString(1), arguments.get(2, LocalDate.class));
    assertFalse(book.isNewRelease(NewReleaseArgumentsProvider.TODAY));
}

An ArgumentsAggregator moves that mapping out of the test, so the test method receives a ready Book. JUnit creates the aggregator through its constructor, so an aggregator declared inside the test class must be a static nested class.

static class BookAggregator implements ArgumentsAggregator {

    @Override
    public Book aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) {
        return new Book(arguments.getString(0), arguments.getString(1), arguments.get(2, LocalDate.class));
    }
}
@ParameterizedTest
@CsvSource({
    "Dune, Frank Herbert, 1965-08-01",
    "Emma, Jane Austen,   1815-12-23"
})
void readsWithAggregator(@AggregateWith(BookAggregator.class) Book book) {
    assertEquals(4, book.title().length());
}

12. Custom Display Names

The default display name is [{index}] {argumentSetNameOrArgumentsWithNames}. It shows the invocation number and either the name of an argument set or the arguments. The attribute name of @ParameterizedTest replaces it with our own pattern.

PlaceholderReplaced with
{displayName}the display name of the method
{index}the invocation number, starting at 1
{arguments}all arguments, separated by commas
{argumentsWithNames}all arguments with their parameter names
{argumentSetName}the name given with argumentSet()
{0}, {1}, …a single argument, with MessageFormat patterns such as {0,number,#.##}

Parameter names appear only when the test classes are compiled with the -parameters flag. Without it, {argumentsWithNames} shows the values only, unless the names come from CSV headers. A project-wide default pattern goes in the configuration parameter junit.jupiter.params.displayname.default.

In JUnit 6, text arguments are quoted in display names, so a name pattern written for JUnit 5 shows extra quotes after the upgrade. The intro example sets quoteTextArguments = false to keep the old style, and without it the names read “BASIC”: “2” x “10.00” = “20.00”, because every CSV value is text.

13. Parameterized Classes With @ParameterizedClass

Sometimes every test of a class must run for every input, for example all pricing rules for every membership tier. Repeating the same source on each method duplicates the data. The annotation @ParameterizedClass (since JUnit 5.13, experimental in JUnit 6.1) parameterizes the whole class instead. JUnit runs all tests of the class once per argument set.

@ParameterizedClass
@EnumSource(MembershipTier.class)
class TierPricingTest {

    @Parameter
    MembershipTier tier;

    @Test
    void neverChargesMoreThanListPrice() {
        BigDecimal price = calculator.finalPrice(new BigDecimal("20.00"), 1, tier);
        assertTrue(price.compareTo(new BigDecimal("20.00")) <= 0);
    }

    @Test
    void keepsTwoDecimalPlaces() {
        assertEquals(2, calculator.finalPrice(new BigDecimal("9.99"), 3, tier).scale());
    }
}
BASIC > neverChargesMoreThanListPrice()
BASIC > keepsTwoDecimalPlaces()
SILVER > neverChargesMoreThanListPrice()
SILVER > keepsTwoDecimalPlaces()
GOLD > neverChargesMoreThanListPrice()
GOLD > keepsTwoDecimalPlaces()

The arguments go into fields annotated with @Parameter, or into the constructor when no such field exists. All sources of this article work on a class, and the methods annotated with @BeforeParameterizedClassInvocation and @AfterParameterizedClassInvocation run around each invocation of the class. Because the API is still experimental, details may change in a later 6.x release.

14. Common Configuration Errors

Most failures of new parameterized tests are configuration errors, not test failures. Surefire reports them as errors with a clear message.

  • A source that supplies no arguments at all fails with TemplateInvocationValidationException, unless the test sets allowZeroInvocations = true.
  • A source with more columns than the method has parameters is ignored by default. With argumentCountValidation = ArgumentCountValidationMode.STRICT, or the configuration parameter junit.jupiter.params.argumentCountValidation=strict, it fails.
  • A non-static factory method or field in a test class without PER_CLASS lifecycle fails the test.
  • A value that JUnit cannot convert, such as abc for an int parameter, fails with a ParameterResolutionException caused by an ArgumentConversionException.
@ParameterizedTest(argumentCountValidation = ArgumentCountValidationMode.STRICT)
@CsvSource({"10001, 94105"})
void tooManyColumns(String zip) {
    assertTrue(new ZipCodeValidator().isValid(zip));
}

static Stream<String> zipsFromEmptyImport() {
    return Stream.empty();
}

@ParameterizedTest
@MethodSource("zipsFromEmptyImport")
void noArguments(String zip) {
    assertTrue(new ZipCodeValidator().isValid(zip));
}
Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.
Note: the provided arguments were [10001, 94105]
org.junit.jupiter.api.extension.TemplateInvocationValidationException: Configuration error: You must configure at least one set of arguments for this @ParameterizedTest

We turn on strict mode for the whole project in junit-platform.properties, because a forgotten column often means a forgotten assertion.

15. Parameterized Tests FAQs

Dependencies, null values, the choice of source and the JUnit 4 migration are the typical follow-up topics after a first parameterized test.

15.1. Do We Need the junit-jupiter-params Dependency?

Only when the project declares the JUnit modules one by one. The aggregate junit-jupiter artifact already depends on junit-jupiter-params.

15.2. How Do We Pass null to a Parameterized Test?

With @NullSource or @NullAndEmptySource, with an empty unquoted value in @CsvSource, with the nullValues attribute, or with null inside Arguments.of() in a factory method. @ValueSource cannot supply it.

15.3. What Is the Difference Between @MethodSource and @FieldSource?

Both take test data from code. @MethodSource calls a method, so it can compute the data on every call. @FieldSource reads a field, which suits constant data such as a List, and needs a Supplier for streams.

15.4. Can We Combine @ParameterizedTest With @RepeatedTest?

No. Both make the method a test template, and the repetitions have no arguments, so they fail with ParameterResolutionException. The JUnit @RepeatedTest article compares the two annotations.

15.5. How Do We Migrate JUnit 4 Parameterized Tests?

JUnit 4 used @RunWith(Parameterized.class) with a @Parameters method and constructor injection for the whole class. In JUnit 5 and 6, a single method uses @ParameterizedTest with a source, and a whole class uses @ParameterizedClass, which is the closest match to the JUnit 4 runner.

16. Conclusion

A parameterized test runs one method for many argument sets. @ValueSource and @EnumSource cover single values, @NullAndEmptySource adds the edge cases, @CsvSource and @CsvFileSource handle tables, and @MethodSource, @FieldSource and @ArgumentsSource supply objects from code. JUnit converts the arguments, and ArgumentsAccessor or an aggregator turns a row into an object.

On JUnit 6, display names quote text arguments, CSV parsing uses FastCSV, and @ParameterizedClass runs a whole test class per argument set. A strict argument count check and readable name patterns make a failing invocation simple to find in the report.

17. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. I want to use an @EnumSource annotation for my test, but for each element in the enum, I want to specify the expected result, which is different for each enum element. How do I do this?

    • Different results mean different assertions, which is against the best practice. A unit test should fail only for a single reason. Testing multiple scenarios is not recommended.

      Use @EnumSource(value = TimeUnit.class, names = { “DAYS”, “HOURS” }) to test the similar enums in a test case. In other testcases, you can exclude the enums you do not want to test. For example, @EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { “DAYS”, “HOURS” })

      • Hi Lokesh, perhaps I wasn’t clear enough. I am testing a single method which takes an enum value as a parameter and returns a single value. The return value depends on the enum parameter. I don’t believe that this is “testing multiple scenarios”, but if it is, I don’t understand the point of the @EnumSource at all. I now have to replace the @EnumSource with an @CsvSource, where I can specify each enum value plus its expected result.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.