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?
answer
- @ParameterizedTest + @CsvSource, one row = one invocation
- Split on comma, bound positionally
- Column count must match parameter count
- Text in, implicit conversion to parameter types
- 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@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
Be able to write the annotation from memory, explain one row equals one invocation, and state that columns bind positionally to parameters.
Add the defaults — comma delimiter, whitespace trimming, single-quote quoting — and the fact that every cell starts life as a String before conversion.
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.
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