From 550c585e0580beb0ecae39ebc9e6fd49db7b2562 Mon Sep 17 00:00:00 2001 From: asmhatre Date: Thu, 1 Oct 2026 16:20:34 +0000 Subject: [PATCH] Add JUnit 6 DisplayNameGenerator, @Nested structure, and @Tag filtering demo --- .gitignore | 5 +- DisplayNestedTag/README.md | 89 +++++++++++++ DisplayNestedTag/build.gradle.kts | 45 +++++++ .../01-custom-display-name-generator.txt | 26 ++++ .../02-builtin-generators-comparison.txt | 28 ++++ .../docs/output/03-nested-lifecycle-tree.txt | 26 ++++ .../output/04-nested-ordering-determinism.txt | 10 ++ .../docs/output/05-tag-filter-fast.txt | 13 ++ .../output/06-tag-filter-exclude-slow.txt | 33 +++++ .../docs/output/07-tag-filter-expression.txt | 13 ++ .../docs/output/08-full-suite.txt | 37 ++++++ .../output/09-tags-and-disabled-reason.txt | 25 ++++ .../output/10-displaynamegenerator-javap.txt | 38 ++++++ DisplayNestedTag/pom.xml | 46 +++++++ DisplayNestedTag/settings.gradle.kts | 1 + .../BuiltInGeneratorsComparisonTest.java | 74 +++++++++++ .../CustomDisplayNameGeneratorTest.java | 84 ++++++++++++ .../junit/displaynestedtag/FastUnitCheck.java | 23 ++++ .../OrderPricingNestedTest.java | 124 ++++++++++++++++++ .../OrderRepositoryIntegrationTest.java | 62 +++++++++ .../SentenceCaseDisplayNameGenerator.java | 65 +++++++++ .../ValidationChecksTest.java | 62 +++++++++ 22 files changed, 928 insertions(+), 1 deletion(-) create mode 100644 DisplayNestedTag/README.md create mode 100644 DisplayNestedTag/build.gradle.kts create mode 100644 DisplayNestedTag/docs/output/01-custom-display-name-generator.txt create mode 100644 DisplayNestedTag/docs/output/02-builtin-generators-comparison.txt create mode 100644 DisplayNestedTag/docs/output/03-nested-lifecycle-tree.txt create mode 100644 DisplayNestedTag/docs/output/04-nested-ordering-determinism.txt create mode 100644 DisplayNestedTag/docs/output/05-tag-filter-fast.txt create mode 100644 DisplayNestedTag/docs/output/06-tag-filter-exclude-slow.txt create mode 100644 DisplayNestedTag/docs/output/07-tag-filter-expression.txt create mode 100644 DisplayNestedTag/docs/output/08-full-suite.txt create mode 100644 DisplayNestedTag/docs/output/09-tags-and-disabled-reason.txt create mode 100644 DisplayNestedTag/docs/output/10-displaynamegenerator-javap.txt create mode 100644 DisplayNestedTag/pom.xml create mode 100644 DisplayNestedTag/settings.gradle.kts create mode 100644 DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/BuiltInGeneratorsComparisonTest.java create mode 100644 DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/CustomDisplayNameGeneratorTest.java create mode 100644 DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/FastUnitCheck.java create mode 100644 DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderPricingNestedTest.java create mode 100644 DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderRepositoryIntegrationTest.java create mode 100644 DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/SentenceCaseDisplayNameGenerator.java create mode 100644 DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/ValidationChecksTest.java diff --git a/.gitignore b/.gitignore index ec1248c..de7d56c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ hs_err_pid* replay_pid* #IntelliJ IDEA -.idea/ \ No newline at end of file +.idea/ + +# Maven build output +target/ \ No newline at end of file diff --git a/DisplayNestedTag/README.md b/DisplayNestedTag/README.md new file mode 100644 index 0000000..afedfdb --- /dev/null +++ b/DisplayNestedTag/README.md @@ -0,0 +1,89 @@ +# JUnit 6 — Display Name Generators, Nested Structure, and Tags + +Companion module for [Organising and Displaying JUnit Tests: @DisplayName, @Nested, @Tag +(JUnit 5 → JUnit 6)](https://ankurm.com/organising-displaying-junit-tests-displayname-nested-tag/) +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. + +## 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 (`lastUpdated 20260807`) | +| JDK | **25 (Temurin, LTS)** | build and run; `maven.compiler.release` is set to 17, JUnit 6's actual minimum | +| Maven | 3.9.11 | | +| Maven Surefire Plugin | 3.5.2 | | +| JUnit Platform Console Standalone | 6.1.3 | used for the `--details=tree` transcripts | + +JUnit 6.0.0 reached GA on 2025-09-30. 6.1.3 is the current patch release of the 6.1.x line as of +this writing. See the site's [JUnit 6 Deep Dive](https://ankurm.com/junit-6-deep-dive-mastering-the-next-generation-of-java-testing/) +for the full migration story — this module only exercises what changed for `@DisplayName`, +`@Nested`, and `@Tag` specifically. + +## Quickstart + +```bash +mvn test # run everything, JDK 17+ required +mvn test -Dgroups="fast" # only tests tagged "fast" +mvn test -DexcludedGroups="slow" # everything except tests/classes tagged "slow" +mvn test -Dgroups="unit & !slow" # tag expression: unit AND NOT slow +``` + +Gradle equivalent (translated in [`build.gradle.kts`](build.gradle.kts); this module's own captured +output was produced with Maven, the toolchain installed in the build environment - the Gradle file +is provided so readers on Gradle have the matching syntax, not as a second source of real output): + +```bash +./gradlew test # equivalent of mvn test -DexcludedGroups="slow" (the task's default) +./gradlew fastTest # equivalent of mvn test -Dgroups="fast" +``` + +To see the indented, display-name-driven tree the way the post's output blocks were captured +(Maven's own test output is a flat log, not a tree): + +```bash +mvn -q dependency:get -Dartifact=org.junit.platform:junit-platform-console-standalone:6.1.3 +java -jar ~/.m2/repository/org/junit/platform/junit-platform-console-standalone/6.1.3/junit-platform-console-standalone-6.1.3.jar \ + execute --details=tree --class-path target/test-classes:target/classes \ + --select-class com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest +``` + +## Source files + +| File | Demonstrates | +|---|---| +| [`SentenceCaseDisplayNameGenerator.java`](src/test/java/com/ankurm/tutorials/junit/displaynestedtag/SentenceCaseDisplayNameGenerator.java) | a custom `DisplayNameGenerator` implementing the current (JUnit 6 / 5.12+) three-argument overloads | +| [`CustomDisplayNameGeneratorTest.java`](src/test/java/com/ankurm/tutorials/junit/displaynestedtag/CustomDisplayNameGeneratorTest.java) | `@DisplayNameGeneration` applied class-wide, including to a `@Nested` class, with zero manual `@DisplayName` annotations | +| [`BuiltInGeneratorsComparisonTest.java`](src/test/java/com/ankurm/tutorials/junit/displaynestedtag/BuiltInGeneratorsComparisonTest.java) | `Standard`, `ReplaceUnderscores`, and `IndicativeSentences` (with `@IndicativeSentencesGeneration`) run side by side | +| [`OrderPricingNestedTest.java`](src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderPricingNestedTest.java) | three levels of `@Nested` classes, with a shared mutable list that proves the outer→inner `@BeforeEach` cascade order at assertion time | +| [`ValidationChecksTest.java`](src/test/java/com/ankurm/tutorials/junit/displaynestedtag/ValidationChecksTest.java) | class-level and method-level `@Tag`, `@Disabled` with a reason, and the `@FastUnitCheck` composed annotation in use | +| [`FastUnitCheck.java`](src/test/java/com/ankurm/tutorials/junit/displaynestedtag/FastUnitCheck.java) | a composed annotation: `@Test` + `@Tag("fast")` + `@Tag("unit")` as one meta-annotation | +| [`OrderRepositoryIntegrationTest.java`](src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderRepositoryIntegrationTest.java) | class-level `@Tag("slow")`/`@Tag("integration")` inherited into a `@Nested` class | + +## Captured output + +| File | What it shows | +|---|---| +| [`01-custom-display-name-generator.txt`](docs/output/01-custom-display-name-generator.txt) | real tree output from the custom generator, including its one honest rough edge | +| [`02-builtin-generators-comparison.txt`](docs/output/02-builtin-generators-comparison.txt) | `Standard` vs `ReplaceUnderscores` vs `IndicativeSentences` on the same run | +| [`03-nested-lifecycle-tree.txt`](docs/output/03-nested-lifecycle-tree.txt) | the three-level `@Nested` tree, display names generated from `@DisplayName` this time | +| [`04-nested-ordering-determinism.txt`](docs/output/04-nested-ordering-determinism.txt) | the same class run three times; only the elapsed-time line differs | +| [`05-tag-filter-fast.txt`](docs/output/05-tag-filter-fast.txt) | `mvn test -Dgroups="fast"` — only the fast-tagged class runs | +| [`06-tag-filter-exclude-slow.txt`](docs/output/06-tag-filter-exclude-slow.txt) | `mvn test -DexcludedGroups="slow"` — one class drops out entirely, one method drops out of another | +| [`07-tag-filter-expression.txt`](docs/output/07-tag-filter-expression.txt) | `mvn test -Dgroups="unit & !slow"` — a boolean tag expression | +| [`08-full-suite.txt`](docs/output/08-full-suite.txt) | the unfiltered baseline: 20 run, 1 skipped | +| [`09-tags-and-disabled-reason.txt`](docs/output/09-tags-and-disabled-reason.txt) | the console-standalone tree view, which (unlike Surefire's own summary) prints the `@Disabled` reason text inline: `↷ Waiting on RFC 6531...` | +| [`10-displaynamegenerator-javap.txt`](docs/output/10-displaynamegenerator-javap.txt) | real `javap` output off the `junit-jupiter-api-6.1.3.jar` showing the `DisplayNameGenerator` interface's actual shape and the `Deprecated: true` flag on the pre-5.12 single-`Class` overloads | + +## A diagnostic note, not a defect + +`01-custom-display-name-generator.txt` renders one test as "Depositing apositive amount increases +the balance" instead of "Depositing a positive amount...". `SentenceCaseDisplayNameGenerator` +splits `camelCase` only at a lowercase-then-uppercase boundary, so the lone capital `A` in +`depositingAPositiveAmount...` fuses onto the following word instead of standing alone. This is +left uncorrected on purpose: it is exactly the kind of rough edge a hand-written example would +never show, and the post discusses it rather than hiding it. + +## License + +MIT, matching the rest of this repository. diff --git a/DisplayNestedTag/build.gradle.kts b/DisplayNestedTag/build.gradle.kts new file mode 100644 index 0000000..7aa8fd6 --- /dev/null +++ b/DisplayNestedTag/build.gradle.kts @@ -0,0 +1,45 @@ +// Gradle Kotlin DSL equivalent of pom.xml, for readers who filter tags with Gradle instead of +// Maven. This module's docs/output/ transcripts were captured with Maven (the toolchain actually +// installed in the environment this repo was built in) - this file is provided as a translation, +// not as a second source of captured output. The `includeTags`/`excludeTags` calls below are the +// Gradle counterpart of the `-Dgroups` / `-DexcludedGroups` system properties used in +// docs/output/05-tag-filter-fast.txt and docs/output/06-tag-filter-exclude-slow.txt. +plugins { + java +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:6.1.3")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + useJUnitPlatform { + // Default build: everything except the slow-tagged class/methods. + excludeTags("slow") + } + testLogging { + events("passed", "skipped", "failed") + } +} + +// ./gradlew fastTest -> Gradle equivalent of `mvn test -Dgroups="fast"` +tasks.register("fastTest") { + description = "Runs only tests tagged \"fast\" - the Gradle equivalent of -Dgroups=\"fast\"." + group = "verification" + useJUnitPlatform { + includeTags("fast") + } + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath +} diff --git a/DisplayNestedTag/docs/output/01-custom-display-name-generator.txt b/DisplayNestedTag/docs/output/01-custom-display-name-generator.txt new file mode 100644 index 0000000..55b4fb6 --- /dev/null +++ b/DisplayNestedTag/docs/output/01-custom-display-name-generator.txt @@ -0,0 +1,26 @@ +╷ +├─ JUnit Platform Suite ✔ +├─ JUnit Jupiter ✔ +│ └─ Custom display name generator test ✔ +│ ├─ an explicit @DisplayName always wins over the generator ✔ +│ ├─ → Withdrawing more than the balance throws insufficient funds exception ✔ +│ ├─ → Depositing apositive amount increases the balance ✔ +│ └─ When the account is overdrawn ✔ +│ ├─ → Further withdrawals are rejected ✔ +│ └─ → A deposit that covers the overdraft clears it ✔ +└─ JUnit Vintage ✔ + +Test run finished after 123 ms +[ 5 containers found ] +[ 0 containers skipped ] +[ 5 containers started ] +[ 0 containers aborted ] +[ 5 containers successful ] +[ 0 containers failed ] +[ 5 tests found ] +[ 0 tests skipped ] +[ 5 tests started ] +[ 0 tests aborted ] +[ 5 tests successful ] +[ 0 tests failed ] + diff --git a/DisplayNestedTag/docs/output/02-builtin-generators-comparison.txt b/DisplayNestedTag/docs/output/02-builtin-generators-comparison.txt new file mode 100644 index 0000000..37852e0 --- /dev/null +++ b/DisplayNestedTag/docs/output/02-builtin-generators-comparison.txt @@ -0,0 +1,28 @@ +╷ +├─ JUnit Platform Suite ✔ +├─ JUnit Jupiter ✔ +│ └─ BuiltInGeneratorsComparisonTest ✔ +│ ├─ A stack ✔ +│ │ └─ that already has one element ✔ +│ │ └─ grows by one after a second push ✔ +│ ├─ replace underscores generator ✔ +│ │ ├─ a stack with one element reports isEmpty false ✔ +│ │ └─ an empty stack reports isEmpty true ✔ +│ └─ Standard (default) generator - unmodified method names ✔ +│ └─ pushThenPopReturnsTheSameElement() ✔ +└─ JUnit Vintage ✔ + +Test run finished after 147 ms +[ 8 containers found ] +[ 0 containers skipped ] +[ 8 containers started ] +[ 0 containers aborted ] +[ 8 containers successful ] +[ 0 containers failed ] +[ 4 tests found ] +[ 0 tests skipped ] +[ 4 tests started ] +[ 0 tests aborted ] +[ 4 tests successful ] +[ 0 tests failed ] + diff --git a/DisplayNestedTag/docs/output/03-nested-lifecycle-tree.txt b/DisplayNestedTag/docs/output/03-nested-lifecycle-tree.txt new file mode 100644 index 0000000..54d19be --- /dev/null +++ b/DisplayNestedTag/docs/output/03-nested-lifecycle-tree.txt @@ -0,0 +1,26 @@ +╷ +├─ JUnit Platform Suite ✔ +├─ JUnit Jupiter ✔ +│ └─ Order pricing ✔ +│ ├─ an order with no items has a zero total ✔ +│ └─ when a discount code is applied ✔ +│ ├─ the total reflects the discount ✔ +│ └─ and the order also qualifies for free shipping ✔ +│ ├─ shipping cost does not change the total ✔ +│ └─ lifecycle methods ran outer, then discount, then shipping, in that order ✔ +└─ JUnit Vintage ✔ + +Test run finished after 120 ms +[ 6 containers found ] +[ 0 containers skipped ] +[ 6 containers started ] +[ 0 containers aborted ] +[ 6 containers successful ] +[ 0 containers failed ] +[ 4 tests found ] +[ 0 tests skipped ] +[ 4 tests started ] +[ 0 tests aborted ] +[ 4 tests successful ] +[ 0 tests failed ] + diff --git a/DisplayNestedTag/docs/output/04-nested-ordering-determinism.txt b/DisplayNestedTag/docs/output/04-nested-ordering-determinism.txt new file mode 100644 index 0000000..de2536b --- /dev/null +++ b/DisplayNestedTag/docs/output/04-nested-ordering-determinism.txt @@ -0,0 +1,10 @@ +$ for i in 1 2 3; do java -jar junit-platform-console-standalone-6.1.3.jar execute \ + --details=tree --select-class ...OrderPricingNestedTest > run_$i.txt; done +$ diff <(grep -v "Test run finished" run_1.txt) <(grep -v "Test run finished" run_3.txt) + +(no output from diff = the tree, including order, is byte-identical across all 3 runs) + +Only the elapsed-time line differs between runs: +run 1: Test run finished after 120 ms +run 2: Test run finished after 171 ms +run 3: Test run finished after 101 ms diff --git a/DisplayNestedTag/docs/output/05-tag-filter-fast.txt b/DisplayNestedTag/docs/output/05-tag-filter-fast.txt new file mode 100644 index 0000000..0e30a08 --- /dev/null +++ b/DisplayNestedTag/docs/output/05-tag-filter-fast.txt @@ -0,0 +1,13 @@ +$ mvn test -Dgroups="fast" + +[INFO] T E S T S +[INFO] ------------------------------------------------------- +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.ValidationChecksTest +[WARNING] Tests run: 5, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.102 s -- in com.ankurm.tutorials.junit.displaynestedtag.ValidationChecksTest +[INFO] +[INFO] Results: +[INFO] +[WARNING] Tests run: 5, Failures: 0, Errors: 0, Skipped: 1 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS diff --git a/DisplayNestedTag/docs/output/06-tag-filter-exclude-slow.txt b/DisplayNestedTag/docs/output/06-tag-filter-exclude-slow.txt new file mode 100644 index 0000000..72cf868 --- /dev/null +++ b/DisplayNestedTag/docs/output/06-tag-filter-exclude-slow.txt @@ -0,0 +1,33 @@ +$ mvn test -DexcludedGroups="slow" + +[INFO] T E S T S +[INFO] ------------------------------------------------------- +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest$WhenADiscountCodeIsApplied +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest$WhenADiscountCodeIsApplied$AndTheOrderAlsoQualifiesForFreeShipping +[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 s -- in com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest$WhenADiscountCodeIsApplied$AndTheOrderAlsoQualifiesForFreeShipping +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.018 s -- in com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest$WhenADiscountCodeIsApplied +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.116 s -- in com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.ValidationChecksTest +[WARNING] Tests run: 4, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.023 s -- in com.ankurm.tutorials.junit.displaynestedtag.ValidationChecksTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$IndicativeSentenceReportTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$IndicativeSentenceReportTest$that_already_has_one_element +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.009 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$IndicativeSentenceReportTest$that_already_has_one_element +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$IndicativeSentenceReportTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$replace_underscores_generator +[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.019 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$replace_underscores_generator +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$StandardReportTest +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$StandardReportTest +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.050 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.CustomDisplayNameGeneratorTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.CustomDisplayNameGeneratorTest$WhenTheAccountIsOverdrawn +[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 s -- in com.ankurm.tutorials.junit.displaynestedtag.CustomDisplayNameGeneratorTest$WhenTheAccountIsOverdrawn +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.019 s -- in com.ankurm.tutorials.junit.displaynestedtag.CustomDisplayNameGeneratorTest +[INFO] +[INFO] Results: +[INFO] +[WARNING] Tests run: 17, Failures: 0, Errors: 0, Skipped: 1 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS diff --git a/DisplayNestedTag/docs/output/07-tag-filter-expression.txt b/DisplayNestedTag/docs/output/07-tag-filter-expression.txt new file mode 100644 index 0000000..69d0bfd --- /dev/null +++ b/DisplayNestedTag/docs/output/07-tag-filter-expression.txt @@ -0,0 +1,13 @@ +$ mvn test -Dgroups="unit & !slow" + +[INFO] T E S T S +[INFO] ------------------------------------------------------- +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.ValidationChecksTest +[WARNING] Tests run: 4, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.071 s -- in com.ankurm.tutorials.junit.displaynestedtag.ValidationChecksTest +[INFO] +[INFO] Results: +[INFO] +[WARNING] Tests run: 4, Failures: 0, Errors: 0, Skipped: 1 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS diff --git a/DisplayNestedTag/docs/output/08-full-suite.txt b/DisplayNestedTag/docs/output/08-full-suite.txt new file mode 100644 index 0000000..067d67b --- /dev/null +++ b/DisplayNestedTag/docs/output/08-full-suite.txt @@ -0,0 +1,37 @@ +$ mvn test + +[INFO] T E S T S +[INFO] ------------------------------------------------------- +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest$WhenADiscountCodeIsApplied +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest$WhenADiscountCodeIsApplied$AndTheOrderAlsoQualifiesForFreeShipping +[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.010 s -- in com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest$WhenADiscountCodeIsApplied$AndTheOrderAlsoQualifiesForFreeShipping +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.025 s -- in com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest$WhenADiscountCodeIsApplied +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.107 s -- in com.ankurm.tutorials.junit.displaynestedtag.OrderPricingNestedTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.ValidationChecksTest +[WARNING] Tests run: 5, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.035 s -- in com.ankurm.tutorials.junit.displaynestedtag.ValidationChecksTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$IndicativeSentenceReportTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$IndicativeSentenceReportTest$that_already_has_one_element +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.009 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$IndicativeSentenceReportTest$that_already_has_one_element +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.013 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$IndicativeSentenceReportTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$replace_underscores_generator +[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.010 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$replace_underscores_generator +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$StandardReportTest +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest$StandardReportTest +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.032 s -- in com.ankurm.tutorials.junit.displaynestedtag.BuiltInGeneratorsComparisonTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.OrderRepositoryIntegrationTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.OrderRepositoryIntegrationTest$WhenTheIdDoesNotExist +[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.007 s -- in com.ankurm.tutorials.junit.displaynestedtag.OrderRepositoryIntegrationTest$WhenTheIdDoesNotExist +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.016 s -- in com.ankurm.tutorials.junit.displaynestedtag.OrderRepositoryIntegrationTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.CustomDisplayNameGeneratorTest +[INFO] Running com.ankurm.tutorials.junit.displaynestedtag.CustomDisplayNameGeneratorTest$WhenTheAccountIsOverdrawn +[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 s -- in com.ankurm.tutorials.junit.displaynestedtag.CustomDisplayNameGeneratorTest$WhenTheAccountIsOverdrawn +[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.037 s -- in com.ankurm.tutorials.junit.displaynestedtag.CustomDisplayNameGeneratorTest +[INFO] +[INFO] Results: +[INFO] +[WARNING] Tests run: 20, Failures: 0, Errors: 0, Skipped: 1 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS diff --git a/DisplayNestedTag/docs/output/09-tags-and-disabled-reason.txt b/DisplayNestedTag/docs/output/09-tags-and-disabled-reason.txt new file mode 100644 index 0000000..2a13319 --- /dev/null +++ b/DisplayNestedTag/docs/output/09-tags-and-disabled-reason.txt @@ -0,0 +1,25 @@ +╷ +├─ JUnit Platform Suite ✔ +├─ JUnit Jupiter ✔ +│ └─ ValidationChecksTest ✔ +│ ├─ an email with a local part, @, and domain is accepted ✔ +│ ├─ validating ten thousand emails stays correct at volume ✔ +│ ├─ a blank email is rejected ✔ +│ ├─ an internationalised domain name is accepted ↷ Waiting on RFC 6531 (internationalised addresses) support - tracked as TUT-142 +│ └─ an email without an @ is rejected ✔ +└─ JUnit Vintage ✔ + +Test run finished after 170 ms +[ 4 containers found ] +[ 0 containers skipped ] +[ 4 containers started ] +[ 0 containers aborted ] +[ 4 containers successful ] +[ 0 containers failed ] +[ 5 tests found ] +[ 1 tests skipped ] +[ 4 tests started ] +[ 0 tests aborted ] +[ 4 tests successful ] +[ 0 tests failed ] + diff --git a/DisplayNestedTag/docs/output/10-displaynamegenerator-javap.txt b/DisplayNestedTag/docs/output/10-displaynamegenerator-javap.txt new file mode 100644 index 0000000..b2f3f7c --- /dev/null +++ b/DisplayNestedTag/docs/output/10-displaynamegenerator-javap.txt @@ -0,0 +1,38 @@ +$ unzip -o -q junit-jupiter-api-6.1.3.jar org/junit/jupiter/api/DisplayNameGenerator*.class -d /tmp/jprobe +$ javap -p -cp /tmp/jprobe org.junit.jupiter.api.DisplayNameGenerator # interface shape + +Compiled from "DisplayNameGenerator.java" +public interface org.junit.jupiter.api.DisplayNameGenerator { + public static final java.lang.String DEFAULT_GENERATOR_PROPERTY_NAME; + public abstract java.lang.String generateDisplayNameForClass(java.lang.Class); + public default java.lang.String generateDisplayNameForNestedClass(java.lang.Class); + public default java.lang.String generateDisplayNameForNestedClass(java.util.List>, java.lang.Class); + public default java.lang.String generateDisplayNameForMethod(java.lang.Class, java.lang.reflect.Method); + public default java.lang.String generateDisplayNameForMethod(java.util.List>, java.lang.Class, java.lang.reflect.Method); + public static java.lang.String parameterTypesAsString(java.lang.reflect.Method); + public static org.junit.jupiter.api.DisplayNameGenerator getDisplayNameGenerator(java.lang.Class); +} + +$ javap -p -v -cp /tmp/jprobe org.junit.jupiter.api.DisplayNameGenerator # deprecation flags on the old overloads + + MethodParameters: + Name Flags + nestedClass + Deprecated: true + Signature: #112 // (Ljava/lang/Class<*>;)Ljava/lang/String; + RuntimeVisibleAnnotations: + 0: #102(#103=e#104.#122,#106=s#123) + org.apiguardian.api.API( + status=Lorg/apiguardian/api/API$Status;.DEPRECATED + since="5.12" +-- + Name Flags + testClass + testMethod + Deprecated: true + Signature: #134 // (Ljava/lang/Class<*>;Ljava/lang/reflect/Method;)Ljava/lang/String; + RuntimeVisibleAnnotations: + 0: #102(#103=e#104.#122,#106=s#123) + org.apiguardian.api.API( + status=Lorg/apiguardian/api/API$Status;.DEPRECATED + since="5.12" diff --git a/DisplayNestedTag/pom.xml b/DisplayNestedTag/pom.xml new file mode 100644 index 0000000..99bb910 --- /dev/null +++ b/DisplayNestedTag/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + com.ankurTutorials + display-nested-tag-demo + 1.0-SNAPSHOT + + + 17 + UTF-8 + 6.1.3 + + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + + + + + org.junit.jupiter + junit-jupiter + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + + diff --git a/DisplayNestedTag/settings.gradle.kts b/DisplayNestedTag/settings.gradle.kts new file mode 100644 index 0000000..e51ae6a --- /dev/null +++ b/DisplayNestedTag/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "display-nested-tag-demo" diff --git a/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/BuiltInGeneratorsComparisonTest.java b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/BuiltInGeneratorsComparisonTest.java new file mode 100644 index 0000000..7adb8df --- /dev/null +++ b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/BuiltInGeneratorsComparisonTest.java @@ -0,0 +1,74 @@ +package com.ankurm.tutorials.junit.displaynestedtag; + +import java.util.ArrayDeque; +import java.util.Deque; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.IndicativeSentencesGeneration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Same {@code Deque} under test, three different built-in {@link DisplayNameGenerator} + * implementations, run side by side so the console output shows exactly what each one does to + * the same method names. None of these three nested classes carries a single manual + * {@code @DisplayName} on a test method — {@code StandardReportTest} is the only one that + * uses {@code @DisplayName} at all, and only to label itself. + */ +class BuiltInGeneratorsComparisonTest { + + @Nested + @DisplayName("Standard (default) generator - unmodified method names") + class StandardReportTest { + + @Test + void pushThenPopReturnsTheSameElement() { + Deque stack = new ArrayDeque<>(); + stack.push("a"); + assertEquals("a", stack.pop()); + } + } + + @Nested + @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) + class replace_underscores_generator { + + @Test + void an_empty_stack_reports_isEmpty_true() { + Deque stack = new ArrayDeque<>(); + assertTrue(stack.isEmpty()); + } + + @Test + void a_stack_with_one_element_reports_isEmpty_false() { + Deque stack = new ArrayDeque<>(); + stack.push("a"); + assertTrue(!stack.isEmpty()); + } + } + + @Nested + @IndicativeSentencesGeneration(separator = " -> ", generator = DisplayNameGenerator.ReplaceUnderscores.class) + @DisplayName("A stack") + class IndicativeSentenceReportTest { + + @Nested + @DisplayName("that already has one element") + class that_already_has_one_element { + + @Test + @DisplayName("grows by one after a second push") + void growsByOneAfterASecondPush() { + Deque stack = new ArrayDeque<>(); + stack.push("a"); + stack.push("b"); + assertEquals(2, stack.size()); + } + } + } +} diff --git a/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/CustomDisplayNameGeneratorTest.java b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/CustomDisplayNameGeneratorTest.java new file mode 100644 index 0000000..c87dcbc --- /dev/null +++ b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/CustomDisplayNameGeneratorTest.java @@ -0,0 +1,84 @@ +package com.ankurm.tutorials.junit.displaynestedtag; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Applies {@link SentenceCaseDisplayNameGenerator} to a whole class instead of annotating every + * method with {@code @DisplayName}. Every test method and nested class below has NO + * {@code @DisplayName} annotation at all — the generator produces every name shown in the + * test report. + */ +@DisplayNameGeneration(SentenceCaseDisplayNameGenerator.class) +class CustomDisplayNameGeneratorTest { + + @Test + void withdrawingMoreThanTheBalanceThrowsInsufficientFundsException() { + Account account = new Account(50); + assertThrows(InsufficientFundsException.class, () -> account.withdraw(100)); + } + + @Test + void depositingAPositiveAmountIncreasesTheBalance() { + Account account = new Account(50); + account.deposit(25); + assertEquals(75, account.getBalance()); + } + + @Test + @DisplayName("an explicit @DisplayName always wins over the generator") + void thisMethodNameIsNeverShownAnywhere() { + // The generator never even runs for this method - the engine checks for an explicit + // @DisplayName first and only falls back to the DisplayNameGenerator when one is absent. + assertEquals(100, new Account(100).getBalance()); + } + + @Nested + class WhenTheAccountIsOverdrawn { + + @Test + void furtherWithdrawalsAreRejected() { + Account account = new Account(-10); + assertThrows(InsufficientFundsException.class, () -> account.withdraw(1)); + } + + @Test + void aDepositThatCoversTheOverdraftClearsIt() { + Account account = new Account(-10); + account.deposit(10); + assertEquals(0, account.getBalance()); + } + } + + /** Minimal domain type — this demo is about names, not banking logic. */ + static class Account { + private int balance; + + Account(int openingBalance) { + this.balance = openingBalance; + } + + void withdraw(int amount) { + if (balance < 0 || amount > balance) { + throw new InsufficientFundsException(); + } + balance -= amount; + } + + void deposit(int amount) { + balance += amount; + } + + int getBalance() { + return balance; + } + } + + static class InsufficientFundsException extends RuntimeException { + } +} diff --git a/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/FastUnitCheck.java b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/FastUnitCheck.java new file mode 100644 index 0000000..23d6dbc --- /dev/null +++ b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/FastUnitCheck.java @@ -0,0 +1,23 @@ +package com.ankurm.tutorials.junit.displaynestedtag; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * A composed annotation: {@code @FastUnitCheck} is exactly {@code @Test @Tag("fast") @Tag("unit")} + * in one name. Meta-annotations are picked up transitively by the Jupiter engine, so a method + * annotated only with {@code @FastUnitCheck} is discovered as a test AND carries both tags - no + * repetition needed across a class full of these. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Test +@Tag("fast") +@Tag("unit") +public @interface FastUnitCheck { +} diff --git a/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderPricingNestedTest.java b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderPricingNestedTest.java new file mode 100644 index 0000000..337470c --- /dev/null +++ b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderPricingNestedTest.java @@ -0,0 +1,124 @@ +package com.ankurm.tutorials.junit.displaynestedtag; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Three levels of {@code @Nested} classes built around one shared {@code List} that each + * level's {@code @BeforeEach} mutates further. The {@code callOrder} list is the point: it proves, + * at assertion time, exactly which lifecycle methods ran and in what order for a test three levels + * deep — the same mechanism the post's lifecycle diagram is built from. + */ +@DisplayName("Order pricing") +class OrderPricingNestedTest { + + private List callOrder; + + @BeforeEach + void outerSetUp() { + callOrder = new ArrayList<>(); + callOrder.add("outer.beforeEach"); + } + + @AfterEach + void outerTearDown() { + callOrder.add("outer.afterEach"); + } + + @Test + @DisplayName("an order with no items has a zero total") + void emptyOrderHasZeroTotal() { + assertEquals(0, new Order().getTotal()); + assertEquals(List.of("outer.beforeEach"), callOrder); + } + + @Nested + @DisplayName("when a discount code is applied") + class WhenADiscountCodeIsApplied { + + private Order order; + + @BeforeEach + void applyDiscount() { + callOrder.add("discount.beforeEach"); + order = new Order(); + order.addItem(100); + order.applyDiscountPercent(10); + } + + @AfterEach + void afterDiscount() { + callOrder.add("discount.afterEach"); + } + + @Test + @DisplayName("the total reflects the discount") + void totalReflectsDiscount() { + assertEquals(90, order.getTotal()); + } + + @Nested + @DisplayName("and the order also qualifies for free shipping") + class AndTheOrderAlsoQualifiesForFreeShipping { + + @BeforeEach + void applyFreeShipping() { + callOrder.add("shipping.beforeEach"); + order.setFreeShipping(true); + } + + @Test + @DisplayName("shipping cost does not change the total") + void shippingDoesNotChangeTotal() { + assertEquals(90, order.getTotal()); + assertTrue(order.hasFreeShipping()); + } + + @Test + @DisplayName("lifecycle methods ran outer, then discount, then shipping, in that order") + void lifecycleRanOuterToInner() { + // The test itself has not run yet when this assertion executes, so callOrder + // holds exactly the three @BeforeEach calls that led up to it - in cascade order. + assertEquals( + List.of("outer.beforeEach", "discount.beforeEach", "shipping.beforeEach"), + callOrder); + } + } + } + + /** Minimal domain type - the lifecycle composition is the point of this file, not pricing. */ + static class Order { + private int subtotal; + private int discountPercent; + private boolean freeShipping; + + void addItem(int price) { + subtotal += price; + } + + void applyDiscountPercent(int percent) { + this.discountPercent = percent; + } + + void setFreeShipping(boolean freeShipping) { + this.freeShipping = freeShipping; + } + + boolean hasFreeShipping() { + return freeShipping; + } + + int getTotal() { + return subtotal - (subtotal * discountPercent / 100); + } + } +} diff --git a/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderRepositoryIntegrationTest.java b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderRepositoryIntegrationTest.java new file mode 100644 index 0000000..35d431b --- /dev/null +++ b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/OrderRepositoryIntegrationTest.java @@ -0,0 +1,62 @@ +package com.ankurm.tutorials.junit.displaynestedtag; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tagged {@code slow} + {@code integration} at the class level so {@code excludedGroups=slow} + * drops the whole class from a quick build. {@code @Tag} is {@code @Inherited} by the JUnit engine + * across {@code @Nested} classes specifically (this is JUnit's own tag-collection behaviour, not + * plain Java annotation inheritance, which does not apply to member classes at all) - the nested + * class below inherits both tags from its enclosing class without repeating them. + */ +@Tag("slow") +@Tag("integration") +@DisplayName("Order repository") +class OrderRepositoryIntegrationTest { + + private final FakeOrderRepository repository = new FakeOrderRepository(); + + @Test + @DisplayName("a saved order can be retrieved by id") + void savedOrderCanBeRetrievedById() { + String id = repository.save("coffee-beans"); + assertEquals(Optional.of("coffee-beans"), repository.findById(id)); + } + + @Nested + @DisplayName("when the id does not exist") + class WhenTheIdDoesNotExist { + + @Test + @DisplayName("findById returns an empty Optional") + void findByIdReturnsEmpty() { + assertTrue(repository.findById("missing").isEmpty()); + } + } + + /** In-memory stand-in so this module has no real database dependency to install. */ + static class FakeOrderRepository { + private final Map store = new HashMap<>(); + private int nextId; + + String save(String item) { + String id = String.valueOf(++nextId); + store.put(id, item); + return id; + } + + Optional findById(String id) { + return Optional.ofNullable(store.get(id)); + } + } +} diff --git a/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/SentenceCaseDisplayNameGenerator.java b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/SentenceCaseDisplayNameGenerator.java new file mode 100644 index 0000000..1f894c1 --- /dev/null +++ b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/SentenceCaseDisplayNameGenerator.java @@ -0,0 +1,65 @@ +package com.ankurm.tutorials.junit.displaynestedtag; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.DisplayNameGenerator; + +/** + * A {@link DisplayNameGenerator} that turns {@code camelCaseMethodNames} into plain sentences + * without touching a single {@code @DisplayName} annotation. + * + *

This implements the current (JUnit 6 / 5.12+) three-argument overloads that + * take the list of enclosing {@code @Nested} classes, not the single-{@code Class} overloads that + * {@code javap} shows as {@code @Deprecated(since = "5.12")} on {@link DisplayNameGenerator}. The + * deprecated overloads are still called by the engine for a generator that does not override the + * new ones, which is a trap: override the new signature or your generator silently falls back to + * {@link Standard} behaviour for nested classes and parameterised methods. + * + *

Explained in the companion post: + * https://ankurm.com/organising-displaying-junit-tests-displayname-nested-tag/ + */ +public class SentenceCaseDisplayNameGenerator implements DisplayNameGenerator { + + private static final Pattern CAMEL_BOUNDARY = Pattern.compile("(?<=[a-z0-9])(?=[A-Z])"); + + @Override + public String generateDisplayNameForClass(Class testClass) { + return toSentence(testClass.getSimpleName()); + } + + @Override + public String generateDisplayNameForNestedClass(List> enclosingInstanceTypes, Class nestedClass) { + // enclosingInstanceTypes lets a generator see how deep it is nested; we use it only to + // indent, which is enough to show the hierarchy in a flat console run. + String indent = " ".repeat(enclosingInstanceTypes.size()); + return indent + toSentence(nestedClass.getSimpleName()); + } + + @Override + public String generateDisplayNameForMethod(List> enclosingInstanceTypes, Class testClass, Method testMethod) { + String indent = " ".repeat(enclosingInstanceTypes.size() + 1); + return indent + "→ " + toSentence(testMethod.getName()); + } + + private static String toSentence(String camelCaseName) { + String[] words = CAMEL_BOUNDARY.split(camelCaseName); + StringBuilder sentence = new StringBuilder(); + for (int i = 0; i < words.length; i++) { + String word = words[i].toLowerCase(); + sentence.append(i == 0 ? capitalise(word) : word); + if (i < words.length - 1) { + sentence.append(' '); + } + } + return sentence.toString(); + } + + private static String capitalise(String word) { + if (word.isEmpty()) { + return word; + } + return Character.toUpperCase(word.charAt(0)) + word.substring(1); + } +} diff --git a/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/ValidationChecksTest.java b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/ValidationChecksTest.java new file mode 100644 index 0000000..5c4771b --- /dev/null +++ b/DisplayNestedTag/src/test/java/com/ankurm/tutorials/junit/displaynestedtag/ValidationChecksTest.java @@ -0,0 +1,62 @@ +package com.ankurm.tutorials.junit.displaynestedtag; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Class-level {@code @Tag("fast")} and {@code @Tag("unit")} apply to every test below, including + * the one written with the {@link FastUnitCheck} composed annotation - both routes produce the + * same two tags on that method, which {@code docs/output/04-tag-filtering-fast.txt} confirms by + * showing it selected in the "fast" run. + */ +@Tag("fast") +@Tag("unit") +class ValidationChecksTest { + + @Test + @DisplayName("an email without an @ is rejected") + void emailWithoutAtIsRejected() { + assertFalse(isValidEmail("not-an-email")); + } + + @Test + @DisplayName("an email with a local part, @, and domain is accepted") + void wellFormedEmailIsAccepted() { + assertTrue(isValidEmail("reader@ankurm.com")); + } + + @FastUnitCheck + @DisplayName("a blank email is rejected") + void blankEmailIsRejected() { + assertFalse(isValidEmail(" ")); + } + + @Test + @Tag("slow") // method-level tag ADDS to the class-level ones: this test is fast+unit+slow + @DisplayName("validating ten thousand emails stays correct at volume") + void validatesLargeVolumeCorrectly() { + for (int i = 0; i < 10_000; i++) { + assertTrue(isValidEmail("user" + i + "@example.com")); + } + } + + @Test + @Disabled("Waiting on RFC 6531 (internationalised addresses) support - tracked as TUT-142") + @DisplayName("an internationalised domain name is accepted") + void internationalisedDomainIsAccepted() { + assertTrue(isValidEmail("user@ünicöde.example")); + } + + private static boolean isValidEmail(String candidate) { + if (candidate == null || candidate.isBlank()) { + return false; + } + int at = candidate.indexOf('@'); + return at > 0 && at < candidate.length() - 1; + } +}