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?
answer
- annotations cannot hold null → @NullSource
- @EmptySource: "", empty List/Set/Map/array
- @NullAndEmptySource = both, composed
- sources are repeatable → concatenated
- null on a primitive parameter fails
basics
~20 sStack 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.
solid answer
~40 sArgument-source annotations on a `@ParameterizedTest` are repeatable, and the engine concatenates what each one produces. So the idiomatic declaration is: ```java @ParameterizedTest @NullAndEmptySource @ValueSource(strings = {" ", "\t", "\n"}) void rejectsBlankNames(String name) { assertThrows(IllegalArgumentException.class, () -> validator.check(name)); } ``` That runs five invocations: `null`, `""`, and the three whitespace strings. The pieces: `@NullSource` supplies a single `null` argument; `@EmptySource` supplies the empty instance appropriate to the parameter type — `""` for `String`, an empty `List`/`Set`/`Map`, an empty array; `@NullAndEmptySource` is simply both, and is a composed annotation, not a separate mechanism. Two constraints. Each applies to a **single-argument** test method. And `@NullSource` cannot target a primitive parameter — `null` has nothing to unbox into, so it fails at resolution; use the boxed type if you genuinely need a null case.
code
java · 15 lines@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void rejectsBlankNames(String name) {
assertThrows(IllegalArgumentException.class, () -> Validator.requireName(name));
}
// invocations: null, "", " ", "\t", "\n"
@ParameterizedTest
@NullSource // fails: null cannot be unboxed to int
void brokenPrimitive(int value) { }
@ParameterizedTest
@NullSource // fine
void okBoxed(Integer value) { }go deeper
Name the three annotations and show them stacked with a @ValueSource of whitespace strings.
Explain that source annotations are repeatable and concatenate, and know the primitive/@NullSource failure and @EmptySource's supported types.
Add the judgement call: keep the trio in one method only while the expected behaviour is identical, otherwise split so each assertion stays precise.
Position it as boundary-case discipline — the null/empty/blank trio should be a house convention for every string-accepting API, backed by the validation contract rather than added ad hoc.
## Why the dedicated sources exist Java annotations cannot hold `null` as an array element, so a literal source can never express the null case — and `null`, `""` and `" "` are the three inputs most validation bugs actually live in. JUnit therefore ships three small sources that fill precisely that gap. - **`@NullSource`** provides exactly one argument set consisting of a single `null`. - **`@EmptySource`** provides one argument set holding the empty value for the parameter's declared type: `""` for `String`, an empty `List`, `Set` or `Map`, an empty array (primitive or object), and, in more recent JUnit 5 versions, empty instances for a wider set of collection types. - **`@NullAndEmptySource`** is a composed annotation that simply carries both of the above. None takes any attributes; the parameter's type is what decides the value. ## Repeatability is the whole trick JUnit's `@ArgumentsSource`-based annotations are repeatable on a test method. The engine collects every source in declaration order and concatenates their argument sets into one stream of invocations. That is why a validation test can read as a stack of sources: ```java @ParameterizedTest @NullAndEmptySource // null, "" @ValueSource(strings = {" ", "\t"}) // blanks void rejectsBlank(String candidate) { } // 4 invocations ``` Declaration order determines invocation order and therefore the `{index}` values in the report — worth knowing when reading a failure line, not something to depend on. ## Constraints and failure modes **Single argument only.** All three sources supply one value per invocation, so the test method must take one parameter (plus any injected ones such as `TestInfo`). Combining them with a multi-argument source in the same method is a mismatch: the null/empty sources will supply one value to a method expecting two, and the invocation fails to resolve. **Primitives reject null.** `@NullSource` on an `int` parameter cannot work — there is no null `int`. JUnit fails that invocation with a parameter-resolution error. If a null case is meaningful, the parameter must be `Integer`. This is the single most common surprise with these annotations. **`@EmptySource` needs a supported type.** If the parameter type has no defined empty instance (say a domain object, or `Optional`), `@EmptySource` fails with a configuration error rather than inventing something. It is deliberately conservative. **Null-hostile call sites.** A worthwhile note for Kotlin-adjacent or annotated codebases: if the method under test declares a non-nullable parameter enforced at runtime, the null case asserts the enforcement mechanism rather than your validation logic. Decide which behaviour the test is about. ## Why interviewers like this one It is a small mechanism, but the answer reveals whether someone writes edge-case tests at all. Null, empty and blank are the canonical boundary trio for any string input, and the fact that JUnit shipped first-class sources for them is a hint about how often they matter. A candidate who reaches for `@NullAndEmptySource` plus a whitespace `@ValueSource` has written real validation tests; one who proposes a separate `@Test` for null is not wrong, just less fluent — and loses the shared assertion. A reasonable follow-on judgement: keep these cases in the *same* test only while the expected behaviour is identical for all of them. If `null` throws `NullPointerException` while `""` throws `IllegalArgumentException`, forcing them into one parameterized method means asserting a supertype or branching in the body — both of which weaken the test. Split instead. ## How to answer Name all three annotations, state that source annotations are repeatable and concatenate, show the stacked declaration, and volunteer the primitive-parameter caveat. That last detail is the one most candidates learn only by hitting the error.
- Why does @NullSource fail on a method whose parameter is declared as int?There is no null value for a primitive: JUnit would have to unbox null into an int, which is impossible, so the invocation fails during parameter resolution. Declaring the parameter as Integer makes the null case valid. The same reasoning applies to any primitive parameter type.
- When should null, empty and blank NOT share one parameterized test method?When the expected behaviour differs — for example null throwing NullPointerException while empty throws IllegalArgumentException. Merging them then forces you to assert a common supertype or branch inside the test body, which weakens the assertion and hides the distinction. Split into separate methods so each states one expectation precisely.
saying these in an interview costs you the question
- Trying to put null inside a @ValueSource array.
- Using @NullSource on a primitive parameter and expecting it to work.
- Believing only one argument-source annotation may appear on a test method.
- Thinking @EmptySource works for any type, rather than the types with a defined empty instance.
- Assuming @NullAndEmptySource is a distinct engine feature rather than a composition of the other two.