# 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=` — 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.