Add JUnit 6 DisplayNameGenerator, @Nested structure, and @Tag filtering demo
This commit is contained in:
+4
-1
@@ -24,4 +24,7 @@ hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
#IntelliJ IDEA
|
||||
.idea/
|
||||
.idea/
|
||||
|
||||
# Maven build output
|
||||
target/
|
||||
@@ -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.
|
||||
@@ -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<Test>("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
|
||||
}
|
||||
@@ -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 ]
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ]
|
||||
|
||||
@@ -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<?>>, 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.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"
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.ankurTutorials</groupId>
|
||||
<artifactId>display-nested-tag-demo</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<junit.version>6.1.3</junit.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit</groupId>
|
||||
<artifactId>junit-bom</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.2</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = "display-nested-tag-demo"
|
||||
+74
@@ -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<String> 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<String> stack = new ArrayDeque<>();
|
||||
assertTrue(stack.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void a_stack_with_one_element_reports_isEmpty_false() {
|
||||
Deque<String> 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<String> stack = new ArrayDeque<>();
|
||||
stack.push("a");
|
||||
stack.push("b");
|
||||
assertEquals(2, stack.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -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 {
|
||||
}
|
||||
}
|
||||
+23
@@ -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 {
|
||||
}
|
||||
+124
@@ -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<String>} 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -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<String, String> store = new HashMap<>();
|
||||
private int nextId;
|
||||
|
||||
String save(String item) {
|
||||
String id = String.valueOf(++nextId);
|
||||
store.put(id, item);
|
||||
return id;
|
||||
}
|
||||
|
||||
Optional<String> findById(String id) {
|
||||
return Optional.ofNullable(store.get(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -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.
|
||||
*
|
||||
* <p>This implements the <strong>current (JUnit 6 / 5.12+)</strong> 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.
|
||||
*
|
||||
* <p>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<Class<?>> 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<Class<?>> 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);
|
||||
}
|
||||
}
|
||||
+62
@@ -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("[email protected]"));
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user