Files
JUnit_Tutorials/parameterized/README.md
T

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 @MethodSource example's three expected isActive() outcomes could not be satisfied by any single-field "still valid" rule. User here uses a renewalDueDate plus a 3-day grace period plus an enabled flag — see 11-method-source-user-active-status.txt for why all three original expected outcomes now genuinely hold.
  • The original @NullAndEmptySource example used the literal letters "t" and "n" where it clearly meant the escape sequences "\t" and "\n" — the letters are not blank per String.isBlank(), which silently broke the example's own stated premise. Fixed here to use real tab and newline characters.
  • The original custom name template referenced CSV header names directly ({USER_ID}, {ROLE}) alongside useHeadersInDisplayName = true. That is not valid syntax — @ParameterizedTest's name attribute compiles to a real java.text.MessageFormat pattern, which only understands numeric positional indices. Rather than silently fixing this and hiding the mistake, it is kept here as HeaderNamePlaceholderMistakeTest.java, a deliberately @Disabled exhibit with its real failure captured in 06-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.