11 KiB
JUnit 5 @ParameterizedTest — Companion Module
Companion module for The Complete Guide to JUnit 5 @ParameterizedTest: Write Smarter, Faster, and Cleaner Java Tests on ankurm.com. Every code sample and every console transcript quoted in that post comes from the files in this directory — nothing was hand-typed into the article.
This module backs a rewrite of a post that first went up when JUnit 5 was on the 5.x line. Rebuilding every example from scratch against current JUnit surfaced three real bugs in the original article's own code samples (see "What changed from the original post" below) — this is not a cosmetic refresh.
Versions this was built and run against
| Component | Version | Notes |
|---|---|---|
| JUnit Jupiter / Platform | 6.1.3 | current GA per maven-metadata.xml on Maven Central at the time of writing — a major-version jump from the 5.x line the original post was written against |
| JDK | 25 (Temurin, LTS) | build and run; maven.compiler.release is set to 17 |
| Maven | 3.9.11 | |
| Maven Surefire Plugin | 3.5.2 | |
| JUnit Platform Console Standalone | 6.1.3 | used only to capture per-invocation display names for docs/output/ — not a runtime dependency of the module itself |
junit-jupiter is the only test-scoped dependency in pom.xml; it transitively pulls in
junit-jupiter-params, so no separate dependency is needed to use @ParameterizedTest and
its argument sources.
Quickstart
mvn test # all 17 test classes, ~3s total, 57 tests, 2 intentionally skipped
mvn test -Dtest=CsvSourceTest # just the @CsvSource examples
mvn test -Dtest=HeaderNamePlaceholderMistakeTest # the disabled "what breaks" exhibit (passes trivially while disabled)
To see the real per-invocation display names this module's post quotes (Surefire's own summary only reports pass/fail counts, not individual names), run the JUnit Platform Console Standalone launcher directly:
mvn -q test-compile
java -jar junit-platform-console-standalone-6.1.3.jar execute --details=tree \
--class-path target/test-classes:target/classes \
--select-class com.ankurm.tutorials.junit.parameterized.CsvSourceTest
To see either of the two deliberately-disabled "what breaks" exhibits fail for real, remove
the @Disabled annotation from the method named in the comment above it and re-run mvn test -Dtest=<ThatClass> — then put the annotation back before committing, which is exactly how
docs/output/06-header-name-placeholder-throws.txt and
docs/output/09-shared-mutable-argument-failure.txt were captured.
Source files
| File | Demonstrates |
|---|---|
ValueSourceTest.java |
@ValueSource — the simplest single-argument source |
NullAndEmptySourceTest.java |
@NullAndEmptySource combined with @ValueSource for a full blank-input sweep |
CsvSourceTest.java |
@CsvSource — inline rows, a custom name, and useHeadersInDisplayName interacting with that custom name |
CsvFileSourceTest.java + postcode-regions.csv |
@CsvFileSource reading real rows from a real classpath file |
EnumSourceTest.java |
@EnumSource in both INCLUDE and EXCLUDE mode over the same enum |
MethodSourceTest.java |
@MethodSource producing real domain objects (User) rather than primitives |
FieldSourceTest.java |
@FieldSource reading arguments from a static field kept in lock-step with production code |
ScenarioArgumentsProvider.java + ArgumentsSourceTest.java |
@ArgumentsSource with a standalone ArgumentsProvider, using the current (non-deprecated) provideArguments(ParameterDeclarations, ExtensionContext) overload |
DashDateConverter.java + ConvertWithTest.java |
@ConvertWith and a custom SimpleArgumentConverter |
MixedAssertionsAntiPatternTest.java |
the BAD shape (two unrelated assertions in one parameterized test) next to the GOOD fix (split into two focused tests) |
DisplayNameTest.java |
a custom name combining {index} with positional placeholders and a literal Unicode character |
HeaderNamePlaceholderMistakeTest.java |
what breaks: a name template referencing a CSV header by name instead of position — kept @Disabled so the build stays green |
SharedMutableArgumentAntiPatternTest.java + Config.java |
what breaks: one shared mutable argument instance handed to every invocation, next to the fresh-instance-per-invocation fix — the failing half kept @Disabled so the build stays green |
Calculator.java, UserService.java, Geocoder.java, Role.java, AuthService.java, User.java, CurrencyService.java, Cms.java, ScenarioProcessor.java |
the small production classes every test above exercises for real — none of them exist solely for the test to pass trivially |
Captured output
| File | What it shows |
|---|---|
01-value-source-palindrome-check.txt |
clean positional display names, and the nested-quoting mistake they fix |
02-null-and-empty-source-blank-usernames.txt |
all five blank-input cases, including real tab and newline characters |
03-csv-source-add-numbers.txt |
the plainest inline @CsvSource shape |
04-csv-source-custom-name-with-headers.txt |
the genuinely new finding: useHeadersInDisplayName changes what a custom name's {0}/{1} placeholders resolve to, not just the default name |
05-csv-source-headers-default-display-name.txt |
the same flag's documented effect on the auto-generated default name, isolated for comparison against 04 |
06-header-name-placeholder-throws.txt |
the real JUnitException/NumberFormatException from referencing a header by name inside a name template |
07-csv-file-source-postcode-region.txt |
@CsvFileSource reading a real file |
08-enum-source-role-write-access.txt |
INCLUDE and EXCLUDE mode together covering every enum constant exactly once |
09-shared-mutable-argument-failure.txt |
the real AssertionFailedError from a shared mutable argument's second invocation |
10-shared-mutable-argument-fixed.txt |
the fresh-instance fix, passing, plus why both display names correctly read callCount=0 |
11-method-source-user-active-status.txt |
@MethodSource producing real User objects with corrected, coherent semantics |
12-field-source-iso-currency-codes.txt |
@FieldSource kept in lock-step with the real production Set |
13-arguments-source-external-scenarios.txt |
a standalone ArgumentsProvider, on the current non-deprecated overload |
14-convert-with-dates-in-2023.txt |
@ConvertWith and a custom SimpleArgumentConverter |
15-mixed-assertions-anti-pattern.txt |
the two-assertions-in-one-test smell next to its single-assertion fix |
16-display-name-custom-multiply.txt |
{index} combined with positional placeholders and a literal Unicode character |
17-full-suite-final.txt |
all 57 tests, exactly as committed, green in under four seconds, 2 intentional skips |
What changed from the original post
Rebuilding every example against current JUnit (rather than copying the original article's code) surfaced three real defects in the original published post, not in JUnit itself:
- The original
@MethodSourceexample's three expectedisActive()outcomes could not be satisfied by any single-field "still valid" rule.Userhere uses arenewalDueDateplus a 3-day grace period plus anenabledflag — see11-method-source-user-active-status.txtfor why all three original expected outcomes now genuinely hold. - The original
@NullAndEmptySourceexample used the literal letters"t"and"n"where it clearly meant the escape sequences"\t"and"\n"— the letters are not blank perString.isBlank(), which silently broke the example's own stated premise. Fixed here to use real tab and newline characters. - The original custom
nametemplate referenced CSV header names directly ({USER_ID},{ROLE}) alongsideuseHeadersInDisplayName = true. That is not valid syntax —@ParameterizedTest'snameattribute compiles to a realjava.text.MessageFormatpattern, which only understands numeric positional indices. Rather than silently fixing this and hiding the mistake, it is kept here asHeaderNamePlaceholderMistakeTest.java, a deliberately@Disabledexhibit with its real failure captured in06-header-name-placeholder-throws.txt.
Two further things were added that the original post didn't cover at all: the
shared-mutable-argument anti-pattern
(SharedMutableArgumentAntiPatternTest.java),
and the interaction between useHeadersInDisplayName and a custom name template's own
positional placeholders (04/05 above) — neither is documented anywhere as clearly as a real,
run, captured failure makes it.
License
MIT, matching the rest of this repository.