Add JUnit 6 DisplayNameGenerator, @Nested structure, and @Tag filtering demo

This commit is contained in:
2026-10-01 16:33:11 +00:00
parent 5ed079dff3
commit 550c585e05
22 changed files with 928 additions and 1 deletions
+3
View File
@@ -25,3 +25,6 @@ replay_pid*
#IntelliJ IDEA #IntelliJ IDEA
.idea/ .idea/
# Maven build output
target/
+89
View File
@@ -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.
+45
View File
@@ -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"
+46
View File
@@ -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>
+1
View File
@@ -0,0 +1 @@
rootProject.name = "display-nested-tag-demo"
@@ -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 &mdash; {@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());
}
}
}
}
@@ -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 &mdash; 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 &mdash; 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 {
}
}
@@ -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 {
}
@@ -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 &mdash; 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);
}
}
}
@@ -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));
}
}
}
@@ -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);
}
}
@@ -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;
}
}