Add parameterized module: JUnit 5 @ParameterizedTest argument sources, display names, and two real anti-pattern failures

This commit is contained in:
Claude
2026-10-03 21:50:52 +00:00
parent 8be1245951
commit ee75bfa792
45 changed files with 1179 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
# JUnit 5 @ParameterizedTest — Companion Module
Companion module for [The Complete Guide to JUnit 5 @ParameterizedTest: Write Smarter,
Faster, and Cleaner Java Tests](https://ankurm.com/the-complete-guide-to-junit-5-parameterizedtest-write-smarter-faster-and-cleaner-java-tests/)
on [ankurm.com](https://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
```bash
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](https://junit.org/junit5/docs/current/user-guide/#running-tests-console-launcher)
launcher directly:
```bash
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`](src/test/java/com/ankurm/tutorials/junit/parameterized/ValueSourceTest.java) | `@ValueSource` — the simplest single-argument source |
| [`NullAndEmptySourceTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/NullAndEmptySourceTest.java) | `@NullAndEmptySource` combined with `@ValueSource` for a full blank-input sweep |
| [`CsvSourceTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/CsvSourceTest.java) | `@CsvSource` — inline rows, a custom `name`, and `useHeadersInDisplayName` interacting with that custom `name` |
| [`CsvFileSourceTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/CsvFileSourceTest.java) + [`postcode-regions.csv`](src/test/resources/test-data/postcode-regions.csv) | `@CsvFileSource` reading real rows from a real classpath file |
| [`EnumSourceTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/EnumSourceTest.java) | `@EnumSource` in both `INCLUDE` and `EXCLUDE` mode over the same enum |
| [`MethodSourceTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/MethodSourceTest.java) | `@MethodSource` producing real domain objects (`User`) rather than primitives |
| [`FieldSourceTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/FieldSourceTest.java) | `@FieldSource` reading arguments from a static field kept in lock-step with production code |
| [`ScenarioArgumentsProvider.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/ScenarioArgumentsProvider.java) + [`ArgumentsSourceTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/ArgumentsSourceTest.java) | `@ArgumentsSource` with a standalone `ArgumentsProvider`, using the current (non-deprecated) `provideArguments(ParameterDeclarations, ExtensionContext)` overload |
| [`DashDateConverter.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/DashDateConverter.java) + [`ConvertWithTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/ConvertWithTest.java) | `@ConvertWith` and a custom `SimpleArgumentConverter` |
| [`MixedAssertionsAntiPatternTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/MixedAssertionsAntiPatternTest.java) | the BAD shape (two unrelated assertions in one parameterized test) next to the GOOD fix (split into two focused tests) |
| [`DisplayNameTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/DisplayNameTest.java) | a custom `name` combining `{index}` with positional placeholders and a literal Unicode character |
| [`HeaderNamePlaceholderMistakeTest.java`](src/test/java/com/ankurm/tutorials/junit/parameterized/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`](src/test/java/com/ankurm/tutorials/junit/parameterized/SharedMutableArgumentAntiPatternTest.java) + [`Config.java`](src/main/java/com/ankurm/tutorials/junit/parameterized/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`](docs/output/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`](docs/output/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`](docs/output/03-csv-source-add-numbers.txt) | the plainest inline `@CsvSource` shape |
| [`04-csv-source-custom-name-with-headers.txt`](docs/output/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`](docs/output/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`](docs/output/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`](docs/output/07-csv-file-source-postcode-region.txt) | `@CsvFileSource` reading a real file |
| [`08-enum-source-role-write-access.txt`](docs/output/08-enum-source-role-write-access.txt) | `INCLUDE` and `EXCLUDE` mode together covering every enum constant exactly once |
| [`09-shared-mutable-argument-failure.txt`](docs/output/09-shared-mutable-argument-failure.txt) | the real `AssertionFailedError` from a shared mutable argument's second invocation |
| [`10-shared-mutable-argument-fixed.txt`](docs/output/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`](docs/output/11-method-source-user-active-status.txt) | `@MethodSource` producing real `User` objects with corrected, coherent semantics |
| [`12-field-source-iso-currency-codes.txt`](docs/output/12-field-source-iso-currency-codes.txt) | `@FieldSource` kept in lock-step with the real production `Set` |
| [`13-arguments-source-external-scenarios.txt`](docs/output/13-arguments-source-external-scenarios.txt) | a standalone `ArgumentsProvider`, on the current non-deprecated overload |
| [`14-convert-with-dates-in-2023.txt`](docs/output/14-convert-with-dates-in-2023.txt) | `@ConvertWith` and a custom `SimpleArgumentConverter` |
| [`15-mixed-assertions-anti-pattern.txt`](docs/output/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`](docs/output/16-display-name-custom-multiply.txt) | `{index}` combined with positional placeholders and a literal Unicode character |
| [`17-full-suite-final.txt`](docs/output/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`](docs/output/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`](src/test/java/com/ankurm/tutorials/junit/parameterized/HeaderNamePlaceholderMistakeTest.java),
a deliberately `@Disabled` exhibit with its real failure captured in
[`06-header-name-placeholder-throws.txt`](docs/output/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`](src/test/java/com/ankurm/tutorials/junit/parameterized/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.