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?
answer
- String source + different parameter type → convert
- ISO-8601 for java.time; enums via exact valueOf
- UUID/Path/URI/BigDecimal/Locale/Currency/Charset/Class
- fallback: single-String static factory or constructor
- widening: int source → long/double param
basics
~20 sJUnit 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.
solid answer
~50 sJupiter applies **implicit argument conversion**. When a source such as `@CsvSource` or `@ValueSource(strings = …)` supplies a `String` and the declared parameter type is something else, the default argument converter parses it — no annotation needed. Built-in targets include: - primitives and their wrappers (`"42"` → `int`), plus **widening** (an `int` source into a `long`/`float`/`double` parameter); - `enum` constants via `valueOf`; - the `java.time` types via their `parse` methods — `LocalDate`, `LocalDateTime`, `Instant`, `Duration`, `Period`, `Year`, `ZoneId`, and the rest; - `File`, `Path`, `URI`, `URL`, `Charset`, `Currency`, `Locale`, `UUID`, `BigDecimal`, `BigInteger`, `Class`. If the target type isn't in that set, JUnit falls back to the type's own single-`String` static factory method or constructor. If nothing matches, you get an `ArgumentConversionException`. Two caveats: implicit conversion only kicks in for `String` sources (a `@MethodSource` returning a real object is passed as-is), and the parse format is the type's canonical one — ISO-8601 for `java.time`, so `"20.05.1990"` needs `@JavaTimeConversionPattern` or a custom converter.
code
java · 19 linesenum Status { ACTIVE, CLOSED }
@ParameterizedTest
@CsvSource({
"ACTIVE, 1990-05-20, 19.99, 7f000101-0000-4000-8000-000000000001",
"CLOSED, 2000-01-01, 5.00, 7f000101-0000-4000-8000-000000000002"
})
void convertsImplicitly(Status status, LocalDate date,
BigDecimal amount, UUID id) {
assertNotNull(status);
assertTrue(amount.signum() > 0);
assertEquals(4, id.version());
}
@ParameterizedTest
@ValueSource(ints = {1, 2, 3}) // widening primitive conversion
void acceptsWiderType(long value) {
assertTrue(value > 0L);
}go deeper
Say that JUnit converts string test data to the declared parameter type automatically and name a few targets: int, enum, LocalDate, UUID.
Add the boundaries: ISO-8601 for java.time, exact enum names, primitive widening, and the single-String factory-method/constructor fallback for your own types.
Lead with diagnosis — read the ArgumentConversionException message, know the null-to-primitive rule, and know when to reach for @JavaTimeConversionPattern or a custom converter.
Frame it as keeping test data readable as text while methods stay strongly typed, and set conventions so data formats are canonical rather than each test inventing a converter.
## What implicit conversion is Argument sources for `@ParameterizedTest` frequently produce text: `@CsvSource` rows are strings, `@ValueSource(strings = …)` is strings, `@CsvFileSource` reads text from a file. Test methods, on the other hand, want real types. Jupiter closes that gap automatically: if the value supplied for a parameter is a `String` and the parameter's declared type is not `String`, the framework's **default argument converter** attempts to convert it. Nothing needs to be annotated. ```java @ParameterizedTest @ValueSource(strings = {"1990-05-20", "2000-01-01"}) void parsesDates(LocalDate date) { assertTrue(date.getYear() >= 1990); } ``` ## The built-in target types The converter has a table of `String`-to-target rules. The important groups: - **Primitives and wrappers** — `boolean/Boolean`, `byte`, `char` (single character), `short`, `int`, `long`, `float`, `double`. `"42"` becomes `42`. - **Enums** — `Enum.valueOf`, so the text must match the constant name exactly, case-sensitively. - **`java.time`** — `Duration`, `Instant`, `LocalDate`, `LocalDateTime`, `LocalTime`, `MonthDay`, `OffsetDateTime`, `OffsetTime`, `Period`, `Year`, `YearMonth`, `ZonedDateTime`, `ZoneId`, `ZoneOffset`, each via its static `parse`, i.e. **ISO-8601 form**. - **Files and locations** — `java.io.File`, `java.nio.file.Path`, `java.net.URI`, `java.net.URL`. - **Misc JDK types** — `java.lang.Class` (via `Class.forName`, so `"java.lang.String"` or `"int"`), `java.math.BigDecimal`/`BigInteger`, `java.nio.charset.Charset`, `java.util.Currency`, `java.util.Locale`, `java.util.UUID`. Beside string conversion, Jupiter also performs **widening primitive conversion**: `@ValueSource(ints = {1, 2})` can feed a `long`, `float` or `double` parameter. And a value already assignable to the parameter type is passed through untouched — conversion only engages when the types differ. ## The fallback for your own types If the target type is not in the built-in table, the converter looks at the *target type itself* for either: - a **factory method**: a non-private, `static` method declared in the target type accepting a single `String` and returning an instance of that type (any name); or - a **factory constructor**: a non-private constructor accepting a single `String`, where the type is a top-level or static nested class. If a factory method and a factory constructor both exist, the factory method wins. If multiple candidate factory methods exist, they are ignored (ambiguous), and conversion fails. This is why a value type like `record Sku(String value) {}` or a class with `public static Sku of(String s)` converts for free from a CSV column, while a type needing two constructor arguments does not. ## Where it does *not* apply - **Non-String sources.** `@MethodSource` returning `Arguments.of(new Order(...), 5)` passes objects straight through; no conversion is attempted, and if the object is not assignable to the parameter type you get a conversion error, not a coercion. - **Non-canonical formats.** `"20.05.1990"` is not ISO-8601, so `LocalDate` conversion fails. Use the built-in `@JavaTimeConversionPattern("dd.MM.yyyy")` or a custom `@ConvertWith` converter. - **Ambiguous or absent factories**, as above. ## Failure modes and their messages When conversion cannot proceed you get an `ArgumentConversionException`, and the message is usually precise enough to fix the test immediately: - *"No built-in converter for source type java.lang.String and target type com.acme.Order"* — your type lacks a single-`String` factory; add one or write a converter. - A `DateTimeParseException` wrapped in a conversion failure — the text does not match ISO-8601. - *"Cannot convert null to primitive value of type int"* — a null argument reached a primitive parameter; declare the parameter as the wrapper type and decide what null means. - An `IllegalArgumentException` from `Enum.valueOf` — the CSV text does not match the constant name (watch case and stray spaces; unquoted `@CsvSource` values are trimmed, but a quoted value keeps its spaces). ## Why it matters in an interview The practical point is that implicit conversion lets test data stay as readable text — CSV rows, external CSV files — while test methods keep real types and real compile-time checking. Knowing the boundary of the built-in table (ISO formats only, single-`String` factories only, no conversion for non-String sources) is what separates "it magically works" from being able to diagnose a red parameterized test in seconds.
- Does implicit conversion apply to values produced by a @MethodSource that returns real objects?No. Implicit conversion is a String-to-target mechanism (plus primitive widening). A method source returning an already-constructed object passes it through unchanged, and if it is not assignable to the declared parameter type the invocation fails with a conversion error. If you want conversion behaviour there, supply the value as a String or annotate the parameter with @ConvertWith.
- Your CSV column holds "active" but the parameter is an enum with constant ACTIVE. What happens?Conversion fails, because enum conversion uses Enum.valueOf, which is case-sensitive and requires an exact constant name. You either fix the data, or write a custom ArgumentConverter that upper-cases before calling valueOf. This is one of the most common causes of a red parameterized test that looks like a data typo.
saying these in an interview costs you the question
- Believing java.time conversion accepts any locale-specific format rather than ISO-8601
- Thinking a custom type converts only with @ConvertWith, missing the single-String factory-method/constructor fallback
- Expecting implicit conversion to coerce objects supplied by @MethodSource
- Assuming enum conversion is case-insensitive
- Thinking a null column silently becomes 0 for an int parameter instead of failing