skip to content

Parameterized Tests

Running one test body over many inputs with @ParameterizedTest and its argument sources, plus converters and readable display names. Interviewers ask because it separates people who copy-paste six near-identical tests from people who table-drive them.

on this pageshow

explore

questions

27

In JUnit 5, how does the @CsvSource annotation supply arguments to a parameterized test method, and how does each row map to that method's parameters?

level: juniorimportance: must knowfreq 62%

answer

  1. @ParameterizedTest + @CsvSource, one row = one invocation
  2. Split on comma, bound positionally
  3. Column count must match parameter count
  4. Text in, implicit conversion to parameter types
  5. Single quote ' is the quote char; whitespace trimmed

basics

~20 s

@CsvSource sits next to @ParameterizedTest and holds an array of comma-separated row strings. JUnit runs the test method once per row, splitting the row on commas and passing the pieces, left to right, as the method's parameters.

solid answer

~50 s

`@CsvSource` is an argument source for `@ParameterizedTest`. You give it rows as strings, e.g. `@CsvSource({"1, 1, 2", "2, 3, 5"})`. The test method is invoked once per row; each row is split on the delimiter (a comma by default) and the resulting columns are bound positionally to the method's parameters. The column count must match the parameter count, otherwise the invocation fails. Everything in the annotation is text, so JUnit applies implicit conversion to reach the declared parameter types — `int`, `LocalDate`, an enum constant, and so on. Leading and trailing whitespace around each column is trimmed by default, which is why `"2, 3, 5"` is readable. Use it when each case is a small tuple of literals — inputs plus the expected result. Once a row needs real objects, collections, or computed values, `@MethodSource` is the better tool. Remember the test method must be annotated `@ParameterizedTest`, not `@Test`.

code

java · 9 lines
java
@ParameterizedTest
@CsvSource({
    "1, 1, 2",
    "2, 3, 5",
    "10, -5, 5"
})
void adds(int a, int b, int expected) {
    assertEquals(expected, Calculator.add(a, b));
}

go deeper

for a junior

Be able to write the annotation from memory, explain one row equals one invocation, and state that columns bind positionally to parameters.

for a middle

Add the defaults — comma delimiter, whitespace trimming, single-quote quoting — and the fact that every cell starts life as a String before conversion.

for a senior

Talk about readability limits: when a table stops being literals it belongs in a file or a factory method, and per-row reporting is the reason to prefer this over a loop.

for a principal

Frame it as test-data strategy — inline tables for local, reviewable cases; externalized data when the table is owned or reviewed by people outside the test file.

## The problem it solves A plain JUnit test method runs once. When the same logic must be checked against many input/output pairs, you either write near-identical methods or loop inside one method — and a loop reports a single test that fails at the first bad case, hiding the rest. JUnit 5's *parameterized tests* solve this: annotate the method with `@ParameterizedTest` instead of `@Test`, add an argument source, and the engine reports one separate test invocation per data row. `@CsvSource` is the argument source for data you want to write inline, as comma-separated text. ## Anatomy ```java @ParameterizedTest @CsvSource({ "1, 1, 2", "2, 3, 5", "10, -5, 5" }) void adds(int a, int b, int expected) { assertEquals(expected, Calculator.add(a, b)); } ``` The annotation's `value` is a `String[]`. **Each array element is one row = one test invocation.** Within a row, the text is split on the delimiter — a comma unless you change it — and the resulting *columns* are bound to the method's parameters **positionally**: first column to first parameter, second to second, and so on. Names are irrelevant; order is everything. Three invocations run here, each reported separately in the IDE and in the build report. If row two fails, rows one and three still run and still pass. ## Column count must match If a row has fewer columns than the method has parameters, the missing parameters cannot be resolved and the invocation fails with a parameter-resolution error. If a row has *more* columns than parameters, JUnit by default also fails the invocation rather than silently dropping the extras. Practically, this means every row in a `@CsvSource` block must be the same shape — a ragged block is a bug, not a feature. One common exception: a trailing parameter of a supported *injected* type (such as `TestInfo` or `TestReporter`) is resolved by the normal extension machinery, not from the CSV, so it does not need a column. ## Everything starts as text CSV is character data, so every column arrives as a `String` and JUnit converts it to the declared parameter type before the call. Primitives and their wrappers, `String`, enums, and many `java.time` types are handled out of the box, so `@CsvSource({"2024-01-31, JANUARY"})` can bind to `(LocalDate date, Month month)`. If the text cannot be converted, the invocation fails with a conversion error naming the offending column — a good reason to keep CSV rows to simple literals. ## Whitespace and quoting defaults By default JUnit trims leading and trailing whitespace from each column (`ignoreLeadingAndTrailingWhitespace = true`), which is what makes aligned, readable rows possible. If a value must keep its spaces, quote it. Note the quote character for `@CsvSource` is the **single quote** `'`, not the double quote: ```java @CsvSource({"' padded ', 9"}) ``` A quoted value may also contain the delimiter: `"'lemon, lime', citrus"` is two columns, not three. ## Where it fits among the sources - `@ValueSource` — one parameter, a flat list of literals. - `@CsvSource` — several parameters per case, still literals; the natural home for input/expected tables. - `@CsvFileSource` — the same table, but stored in a real `.csv` file. - `@MethodSource` / `@ArgumentsSource` — anything that needs constructed objects, collections, or computed data. The honest limit of `@CsvSource` is that its cells are literals. As soon as a case needs a `List`, a builder-constructed domain object, or a value derived at runtime, forcing it through CSV produces unreadable rows plus parsing helpers inside the test — the signal to switch sources. ## Reporting Each invocation gets a generated display name that includes the invocation index and the argument values, so a failure in a big table tells you which row broke without any extra work from you. That per-row visibility — not brevity — is the main reason to prefer a parameterized test over a loop. ## Practical checklist 1. Use `@ParameterizedTest`, never `@Test`, on the method. 2. Keep every row the same column count as the parameter list. 3. Keep cells to literals; quote with `'` when a value contains a comma or meaningful spaces. 4. Keep the table short enough to read in the annotation — beyond roughly a screenful, move it to a file or a factory method.

  • What happens if one row in the @CsvSource block has more columns than the test method has parameters?
    The invocation fails rather than silently ignoring the extra column. JUnit validates the argument count against the parameter list, so a ragged table surfaces as a test error naming the mismatch. The fix is to make every row the same shape, or to declare the extra parameter.
  • How do you pass a value that itself contains a comma?
    Quote it with the annotation's quote character, which for @CsvSource is the single quote: `"'lemon, lime', citrus"` yields two columns. Alternatively set a different `delimiter` or `delimiterString` so the comma stops being special, or set `quoteCharacter` to something else if your data is full of apostrophes.

saying these in an interview costs you the question

  • Leaving @Test on the method alongside @CsvSource, so the row data is never applied
  • Believing the columns bind by parameter name rather than by position
  • Assuming @CsvSource uses double quotes as its quote character
  • Claiming JUnit silently ignores surplus or missing columns
  • Cramming constructed objects or JSON into CSV cells instead of switching to @MethodSource

context

open as a page

In JUnit 5, how does the @MethodSource annotation supply arguments to a parameterized test, and what happens if you leave the annotation's value empty?

level: juniorimportance: must knowfreq 58%

basics

~20 s

@MethodSource names a factory method that returns a Stream (or Iterable/array) of arguments; JUnit runs the test once per element. If you leave the value empty, JUnit looks for a factory method with the same name as the test method.

open as a page

In JUnit 5, a data-driven test method runs once per set of arguments and each run appears as its own entry in the IDE tree and the XML report. How do you control the text shown for each of those per-run entries, and what does that text look like if you do nothing?

level: juniorimportance: must knowfreq 45%

basics

~10 s

Set the name attribute of @ParameterizedTest, e.g. @ParameterizedTest(name = "[{index}] input={0}"). JUnit fills placeholders per invocation: {index} (1-based), {arguments}, {argumentsWithNames}, {displayName}, and positional {0}, {1}. Default pattern is "[{index}] {argumentsWithNames}".

open as a page

You want a JUnit 5 test method to run once for each of the literal inputs 1, 2 and 3 without writing a loop inside the test. Which annotations do you use, and what are the limitations of that argument source?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Mark the method @ParameterizedTest and add @ValueSource(ints = {1, 2, 3}); it runs once per value, passing one argument each time. Limits: exactly one array attribute, only primitives plus String and Class, values must be compile-time constants, and null cannot be supplied.

open as a page

A JUnit 5 parameterized test is fed rows of twelve columns and you don't want a twelve-parameter test method. What does declaring a parameter of type ArgumentsAccessor give you, and how do you read values out of it?

level: middleimportance: must knowfreq 45%

basics

~20 s

Declare one parameter of type ArgumentsAccessor. JUnit passes it the whole argument array for that invocation instead of consuming a single column. You read values positionally: getString(0), getInteger(3), get(5, LocalDate.class), plus size(), toList() and toArray().

open as a page

A JUnit 5 parameterized test supplies the text "1990-05-20" from its argument source, but the test method's parameter is declared as java.time.LocalDate. How does the framework turn the string into that type, and which target types does it handle out of the box?

level: middleimportance: must knowfreq 52%

basics

~20 s

JUnit applies implicit argument conversion: when a supplied String doesn't match the declared parameter type, a built-in converter parses it. It covers primitives and wrappers, enums, java.time types, File/Path, URI/URL, UUID, Locale, Currency, Charset, BigDecimal/BigInteger, Class — plus widening of primitives.

open as a page

What rules must a JUnit 5 @MethodSource factory method satisfy — must it be static, which return types are accepted — and how do you supply several arguments per invocation versus a single one?

level: middleimportance: must knowfreq 50%

basics

~20 s

The factory must be static unless the test class is annotated @TestInstance(PER_CLASS). It may return Stream (including IntStream/LongStream/DoubleStream), Iterable, Iterator, Collection or an array. Each element is an Arguments/Object[] for multi-parameter tests, or a bare value for a single-parameter test.

open as a page

How do you make a JUnit 5 parameterized test method take a single domain object built from several columns of the argument row, using @AggregateWith and a custom ArgumentsAggregator?

level: middleimportance: should knowfreq 34%

basics

~10 s

Write a class implementing ArgumentsAggregator: its aggregateArguments(ArgumentsAccessor, ParameterContext) reads the row positionally and returns the object. Give it a no-arg constructor, then annotate the test parameter @AggregateWith(MyAggregator.class). JUnit passes the built object.

open as a page

How do you take full control of how one argument of a JUnit 5 parameterized test is turned into the parameter's type, using @ConvertWith — and what does the TypedArgumentConverter base class save you compared with implementing ArgumentConverter directly?

level: middleimportance: should knowfreq 38%

basics

~20 s

Annotate the parameter @ConvertWith(MyConverter.class). Implement ArgumentConverter — convert(Object source, ParameterContext context) — or extend TypedArgumentConverter<S,T>, which takes the source and target classes in its constructor and does the type check and null handling, leaving you one typed convert(S) method.

open as a page

How does JUnit 5's @CsvFileSource load its data, what is the difference between its resources and files attributes, and what is numLinesToSkip for?

level: middleimportance: should knowfreq 42%

basics

~20 s

@CsvFileSource reads rows from CSV files instead of the annotation. resources names classpath resources (usually under src/test/resources); files names filesystem paths. numLinesToSkip skips the leading lines — set it to 1 to skip a header row. It defaults to 0.

open as a page

In a JUnit 5 @CsvSource row, what value does the test method receive for a column that is left empty, and how do you deliberately pass an empty String or a null using the nullValues and emptyValue attributes?

level: middleimportance: should knowfreq 46%

basics

~20 s

An unquoted empty column becomes null. A quoted empty column ('') becomes the emptyValue, which defaults to the empty String. Use emptyValue to substitute something else, and nullValues to list tokens such as NIL or N/A that should also convert to null.

open as a page

JUnit 5's @CsvSource accepts either an array of row strings or a single textBlock. How do the two forms differ, and how would you feed it data whose values contain commas or use some other separator character?

level: middleimportance: should knowfreq 44%

basics

~20 s

The array form lists rows as separate quoted strings. The textBlock form puts the whole table in one Java text block, one row per line, so it reads like a real CSV table and supports # comment lines. For odd data, set delimiter/delimiterString or quoteCharacter, or quote the value.

open as a page

How do you point a JUnit 5 @MethodSource at a factory method that lives in a different class, and why would you keep shared test data there rather than duplicating it in each test class?

level: middleimportance: should knowfreq 36%

basics

~10 s

Give the fully qualified class name, then #, then the method name: @MethodSource("com.acme.TestData#validEmails"). The external method must be static. It lets several test classes share one authoritative set of cases instead of copying rows.

open as a page

A JUnit 5 parameterized test reports invocations as "[1] arg0=admin, arg1=403" instead of using the real parameter names. Explain what the {arguments} and {argumentsWithNames} placeholders each produce and why the names degraded to arg0/arg1.

level: middleimportance: should knowfreq 35%

basics

~20 s

{arguments} prints the argument values comma-separated via toString. {argumentsWithNames} prefixes each with its formal parameter name. Parameter names only survive in the class file when compiled with the -parameters flag; without it reflection reports arg0, arg1, so the label degrades.

open as a page

You need a JUnit 5 test to run once per constant of a Java enum, and later to run over only a named subset of those constants. How do you declare that, and how do you express both 'only these' and 'all except these'?

level: middleimportance: should knowfreq 45%

basics

~20 s

Use @ParameterizedTest with @EnumSource. With no attributes it runs over every constant of the enum parameter's type. Restrict with names plus mode: INCLUDE (default) runs only the listed constants, EXCLUDE runs all others, and MATCH_ALL / MATCH_ANY treat the names as regular expressions.

open as a page

A JUnit 5 argument source built from annotation literals cannot supply null, yet null, empty string and blank string are exactly the inputs a validation method must reject. How do you cover all three in one parameterized test?

level: middleimportance: should knowfreq 40%

basics

~20 s

Stack the dedicated sources: @NullSource supplies null, @EmptySource supplies an empty value ("", empty list/set/map/array), @NullAndEmptySource does both, and @ValueSource(strings = {" ", "\t"}) adds blanks. Source annotations are repeatable and their arguments are concatenated.

open as a page

A JUnit 5 parameterized test method mixes plain indexed arguments, an ArgumentsAccessor or @AggregateWith parameter, and a TestInfo injected by the framework. What ordering rules apply to those parameters, and what happens if you get the order wrong?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Order is fixed: indexed arguments first, then aggregator parameters (ArgumentsAccessor or @AggregateWith), then anything supplied by an extension such as TestInfo or TestReporter. Wrong order fails the method up front with an invalid-parameter-order error, not a per-row failure.

open as a page

Inside a JUnit 5 ArgumentsAggregator you need one CSV column as a java.time.LocalDate and another as a custom value type. How does argument conversion work inside aggregation, and can one parameter be both annotated @ConvertWith and @AggregateWith?

level: seniorimportance: should knowfreq 20%

basics

~20 s

Inside an aggregator use accessor.get(index, LocalDate.class) — typed getters run JUnit's implicit conversion. For anything implicit conversion can't handle, call your own parsing (or reuse a converter class) in the aggregator. @ConvertWith and @AggregateWith work at different granularity and are not combined on one parameter.

open as a page

You want a JUnit 5 parameterized test to receive your own value type directly from a CSV column of text, without writing any converter class. What must that type declare for the framework's fallback conversion to work, and what makes the fallback fail?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Declare exactly one non-private static factory method taking a single String and returning that type, or a non-private single-String constructor on a top-level/static nested class. A factory method beats a constructor; several candidate factory methods are ignored, so conversion fails.

open as a page

A JUnit 5 parameterized test fails with messages like "Cannot convert null to primitive value of type int" or "No built-in converter for source type java.lang.String and target type ...". How do you diagnose each, and what are the fixes?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The first means a null argument reached a primitive parameter — declare it as the wrapper type (Integer) and handle null explicitly. The second means the target type is neither built-in nor has a single-String factory method or constructor — add one or annotate the parameter with @ConvertWith.

open as a page

A JUnit 5 suite keeps growing its test tables inside @CsvSource annotations. When would you move that data out into CSV files read by @CsvFileSource, and what problems — encoding, oversized values, quoting, missing files — should you expect after the move?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Externalize when the table is long, shared by several tests, or edited by non-developers. Expect: quote character changes from ' to " , encoding must match the file (UTF-8 default), very long cells hit maxCharsPerColumn, headers need numLinesToSkip, and a missing classpath resource fails the test.

open as a page

When would you implement a custom JUnit 5 ArgumentsProvider and wire it with @ArgumentsSource instead of using @MethodSource, and how do you make such a provider configurable through your own annotation?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Write an ArgumentsProvider when the data source is reusable logic rather than a list — generated, loaded from files, or driven by ExtensionContext. Wire it with @ArgumentsSource(MyProvider.class). Make it configurable by having the provider implement AnnotationConsumer<MyAnnotation> and putting @ArgumentsSource on your own annotation.

open as a page

A large JUnit 5 codebase has hundreds of data-driven test methods and the team wants every one of them to report invocations in the same custom format, without editing every annotation. How would you do that, and which setting wins if a method also declares its own format?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Set the JUnit Platform configuration parameter junit.jupiter.params.displayname.default (in junit-platform.properties on the test classpath, or as a system property) to the desired pattern. Precedence: an explicit name attribute on @ParameterizedTest wins, then the configuration parameter, then JUnit's built-in "[{index}] {argumentsWithNames}".

open as a page

A team's JUnit 5 tests iterate over a Java enum but exclude two constants by name so the suite stays green. Six months later a new constant is added and nothing fails. What went wrong, and how would you set up enum-driven tests so a newly added constant cannot slip through untested?

level: seniorimportance: should knowfreq 28%

basics

~20 s

Excluding by name means new constants are automatically included in the happy-path run but nothing forces anyone to think about them; if the excluded constants are the only ones asserted elsewhere, coverage silently drifts. Prefer exhaustive @EnumSource with no filtering, and pair the constant with its expectation so an unmapped constant fails.

open as a page

JUnit 5.11 added the @FieldSource annotation for parameterized tests. What does it reference, and when would you prefer it to @MethodSource?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

@FieldSource names a static field holding the cases — a List, Collection, Stream supplier or array — instead of a factory method. Prefer it when the data is a fixed constant with no logic; a method is still better when the cases must be computed.

open as a page

As a data-driven JUnit 5 test suite grows, how do you decide between plain indexed parameters, an ArgumentsAccessor read inside the test body, and a custom ArgumentsAggregator — and what is the cost of getting that call wrong?

level: principalimportance: nice to knowfreq 15%

basics

~20 s

Indexed parameters while the row is narrow and each column is a named concept. ArgumentsAccessor for one-off wide or variable-arity rows. A custom aggregator when the same shape recurs or construction is non-trivial. Wrong call costs either unreadable signatures or indirection that hides column meaning.

open as a page

Where do you draw the line between letting a test framework convert textual test data into typed objects and constructing those objects explicitly in test code, and what conventions keep that decision consistent across a large suite?

level: principalimportance: nice to knowfreq 14%

basics

~20 s

Let conversion handle canonical text-to-value mapping — dates, enums, ids, single-value types. Construct explicitly once the object has structure, invariants or business meaning. Conventions: canonical formats, a small shared set of converters, converters that never default or repair, and named composed annotations.

open as a page