skip to content

@CsvSource & @CsvFileSource

Multi-argument rows inline or from classpath files. Interviewers test the parsing details: delimiters, quoting, text blocks, and null handling.

on this pageshow

questions

5

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

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

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