Add parameterized module: JUnit 5 @ParameterizedTest argument sources, display names, and two real anti-pattern failures
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user