skip to content

Argument Conversion

How a CSV string becomes an int, enum, LocalDate, or your own type. Interviewers probe the implicit conversion table and the @ConvertWith escape hatch.

on this pageshow

questions

5

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%

answer

  1. String source + different parameter type → convert
  2. ISO-8601 for java.time; enums via exact valueOf
  3. UUID/Path/URI/BigDecimal/Locale/Currency/Charset/Class
  4. fallback: single-String static factory or constructor
  5. widening: int source → long/double param

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.

solid answer

~50 s

Jupiter 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 lines
java
enum 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

for a junior

Say that JUnit converts string test data to the declared parameter type automatically and name a few targets: int, enum, LocalDate, UUID.

for a middle

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.

for a senior

Lead with diagnosis — read the ArgumentConversionException message, know the null-to-primitive rule, and know when to reach for @JavaTimeConversionPattern or a custom converter.

for a principal

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

context

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

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

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