Add organising module: DisplayNameGenerator/@Nested/@Tag demos on JUnit 6.1.3
Companion code for the rewritten post "Organising and Displaying JUnit Tests: @DisplayName, @Nested, @Tag (JUnit 5 -> JUnit 6)". Same suite also runs green on JUnit 5.14.4 via -Djunit.version. Verified, not assumed: GA dates and versions cross-checked against Maven Central metadata and docs.junit.org release notes; the @TestMethodOrder-inheritance-into-@Nested-classes and MethodOrderer.Default/ClassOrderer.Default behaviour differences between 5.14.4 and 6.1.3 are demonstrated by actually running the same source under both engines (see organising/output/12-* and 14-*), not by reading about them. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YXCrLgRKFgCh9RHKW8xaqJ
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Organising and Displaying JUnit Tests - DisplayNameGenerator, @Nested, @Tag
|
||||||
|
|
||||||
|
Runnable companion to the ankurm.com article "Organising and Displaying JUnit Tests: @DisplayName,
|
||||||
|
@Nested, @Tag (JUnit 5 to JUnit 6)". Every scenario is a small fixture driven through the real
|
||||||
|
JUnit Platform Launcher (see `RunHelper.java`); the tests assert on what actually happened and
|
||||||
|
write it to `output/`, so every transcript quoted in the article is also an assertion that fails
|
||||||
|
the build the day it stops being true.
|
||||||
|
|
||||||
|
## Versions
|
||||||
|
|
||||||
|
| Item | Version |
|
||||||
|
|------|---------|
|
||||||
|
| JUnit (default, `junit.version`) | 6.1.3 (Jupiter, Platform, Launcher - single version line since 6.0) |
|
||||||
|
| Also run green on | 5.14.4 (`mvn test -Djunit.version=5.14.4`) |
|
||||||
|
| JUnit 6.0.0 GA date | September 30, 2025 (confirmed on `docs.junit.org/current/release-notes`) |
|
||||||
|
| Java | 17 language level (`maven.compiler.release`), run on JDK 25.0.4.1 (Temurin, current LTS) |
|
||||||
|
| Maven Surefire | 3.5.4 |
|
||||||
|
|
||||||
|
Both GA numbers were cross-checked against `maven-metadata.xml` on Maven Central
|
||||||
|
(`org/junit/platform/junit-platform-commons` and `org/junit/junit-bom`), not against a blog or an
|
||||||
|
LLM's memory - see the article's versions callout.
|
||||||
|
|
||||||
|
## What each scenario demonstrates
|
||||||
|
|
||||||
|
| Class | Shows | Output |
|
||||||
|
|-------|-------|--------|
|
||||||
|
| `DisplayNameFixtures.PlainDefault` | the default `Standard` generator - raw method signature | `01-default-generator.txt` |
|
||||||
|
| `DisplayNameFixtures.Underscored` | `DisplayNameGenerator.ReplaceUnderscores` | `02-replace-underscores.txt` |
|
||||||
|
| `DisplayNameFixtures.Sentences` | `DisplayNameGenerator.IndicativeSentences` joining class + nested + method into one sentence | `03-indicative-sentences.txt` |
|
||||||
|
| `DisplayNameFixtures.CustomSentenceCase` + `SentenceCaseGenerator` | a hand-written `DisplayNameGenerator` | `04-custom-generator.txt` |
|
||||||
|
| `DisplayNameFixtures.ConfiguredByDefault` | setting the generator project-wide via `junit.jupiter.displayname.generator.default`, no annotation touched | `05-no-config-param.txt`, `06-configured-default-generator.txt` |
|
||||||
|
| `LoanDeskTest` | `@DisplayNameGeneration` + `@Nested` + `@Tag` used together, tag inheritance into nested and doubly-nested classes | real `mvn test` run, `17-mvn-cli-tag-filter.txt` |
|
||||||
|
| `TagFixtures.CatalogServiceTest` | include/exclude filtering, composed tag annotations, OR-vs-AND semantics of `TagFilter.includeTags` | `07`, `08`, `09`, `09b` |
|
||||||
|
| `TagFixtures.TrimmedTagClass` | leading/trailing whitespace in a tag is silently trimmed, not invalid | `10`, `10b` |
|
||||||
|
| `TagFixtures.InternalWhitespaceTagClass` | internal whitespace **is** invalid - logged as a non-critical discovery warning and the tag is dropped, test still runs | `10c`, `10d` |
|
||||||
|
| `TagFixtures.BlankTagClass` | an empty `@Tag("")` inside `@Tags` is dropped the same way | `11` |
|
||||||
|
| `OrderingFixtures.DefaultSiblingOrder` | sibling `@Nested` class order with no orderer declared - deterministic, but a **different** order on 5.14.4 vs 6.1.3 | `12-*-run1/2/3.txt` (stability within a version), `12-*-junit5.14.4.txt` (cross-version diff) |
|
||||||
|
| `OrderingFixtures.ClassOrderOuter` | `@TestClassOrder` reaching two levels of `@Nested` nesting - identical on both versions in this repo's testing | `13-class-order-two-levels.txt` |
|
||||||
|
| `OrderingFixtures.MethodOrderInheritanceOuter` | `@TestMethodOrder` on an outer class now cascading into an unannotated `@Nested` child - **new in JUnit 6.0**, confirmed by a different real method order on 5.14.4 | `14-method-order-inheritance*.txt` |
|
||||||
|
| `src/broken/MethodOrdererDefaultDemo.java` | `MethodOrderer.Default` / `ClassOrderer.Default` (opt a `@Nested` class back out of inherited ordering) added in 6.0 - compiles on 6.1.3, fails on 5.14.4 | `15`, `16` |
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- JDK 17 or higher (built and run here on JDK 25.0.4.1)
|
||||||
|
- Maven 3.6+
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn test # full suite, JUnit 6.1.3
|
||||||
|
mvn test -Djunit.version=5.14.4 # same suite, JUnit 5.14.4
|
||||||
|
mvn test -Dgroups=fast # only @Tag("fast") tests
|
||||||
|
mvn test -DexcludedGroups=slow # everything except @Tag("slow")
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/run-all.sh` regenerates every file in `output/` from a clean state in one command,
|
||||||
|
including the cross-version comparison files and the `src/broken` compiler-message transcripts.
|
||||||
|
Instance ordering in `12-nested-sibling-order-*.txt` is stable within one JUnit version but is
|
||||||
|
explicitly NOT something to hand-derive - it is only ever read from a real run.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
+ DisplayNameFixtures$PlainDefault
|
||||||
|
- total_price_is_zero_when_cart_is_empty()
|
||||||
|
- addingAnItemIncreasesTheCount()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
+ DisplayNameFixtures$Underscored
|
||||||
|
- total price is zero when cart is empty
|
||||||
|
- adding an item increases the count
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
+ A shopping cart
|
||||||
|
- has a total of zero when it is empty
|
||||||
|
+ once an item has been added
|
||||||
|
- has a total equal to that item's price
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
+ Custom sentence case
|
||||||
|
- Should report zero for an empty cart()
|
||||||
|
- Should reject a negative quantity(TestInfo)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
+ DisplayNameFixtures$ConfiguredByDefault
|
||||||
|
- total_price_is_zero_when_cart_is_empty()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
+ DisplayNameFixtures$ConfiguredByDefault
|
||||||
|
- total price is zero when cart is empty
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
catalog.priceLookupIsCaseInsensitive ran
|
||||||
|
catalog.emptyQueryReturnsNoResults ran
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL priceLookupIsCaseInsensitive()
|
||||||
|
SUCCESSFUL emptyQueryReturnsNoResults()
|
||||||
|
SUCCESSFUL TagFixtures$CatalogServiceTest
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
catalog.priceLookupIsCaseInsensitive ran
|
||||||
|
catalog.emptyQueryReturnsNoResults ran
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL priceLookupIsCaseInsensitive()
|
||||||
|
SUCCESSFUL emptyQueryReturnsNoResults()
|
||||||
|
SUCCESSFUL TagFixtures$CatalogServiceTest
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
catalog.reindexingHitsTheRealSearchCluster ran
|
||||||
|
catalog.priceLookupIsCaseInsensitive ran
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL reindexingHitsTheRealSearchCluster()
|
||||||
|
SUCCESSFUL priceLookupIsCaseInsensitive()
|
||||||
|
SUCCESSFUL TagFixtures$CatalogServiceTest
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
catalog.priceLookupIsCaseInsensitive ran
|
||||||
|
catalog.emptyQueryReturnsNoResults ran
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL priceLookupIsCaseInsensitive()
|
||||||
|
SUCCESSFUL emptyQueryReturnsNoResults()
|
||||||
|
SUCCESSFUL TagFixtures$CatalogServiceTest
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
TrimmedTagClass.stillRuns ran
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL stillRuns()
|
||||||
|
SUCCESSFUL TagFixtures$TrimmedTagClass
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
TrimmedTagClass.stillRuns ran
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL stillRuns()
|
||||||
|
SUCCESSFUL TagFixtures$TrimmedTagClass
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
InternalWhitespaceTagClass.stillRunsButTagIsDropped ran
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL stillRunsButTagIsDropped()
|
||||||
|
SUCCESSFUL TagFixtures$InternalWhitespaceTagClass
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
-- results --
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
BlankTagClass.neverActuallyRuns ran
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL neverActuallyRuns()
|
||||||
|
SUCCESSFUL TagFixtures$BlankTagClass
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
+ OrderingFixtures$DefaultSiblingOrder
|
||||||
|
+ Delta
|
||||||
|
- t()
|
||||||
|
+ Alpha
|
||||||
|
- t()
|
||||||
|
+ Mike
|
||||||
|
- t()
|
||||||
|
+ Zebra
|
||||||
|
- t()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
+ OrderingFixtures$DefaultSiblingOrder
|
||||||
|
+ Alpha
|
||||||
|
- t()
|
||||||
|
+ Delta
|
||||||
|
- t()
|
||||||
|
+ Zebra
|
||||||
|
- t()
|
||||||
|
+ Mike
|
||||||
|
- t()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
+ OrderingFixtures$DefaultSiblingOrder
|
||||||
|
+ Alpha
|
||||||
|
- t()
|
||||||
|
+ Delta
|
||||||
|
- t()
|
||||||
|
+ Zebra
|
||||||
|
- t()
|
||||||
|
+ Mike
|
||||||
|
- t()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
+ OrderingFixtures$DefaultSiblingOrder
|
||||||
|
+ Alpha
|
||||||
|
- t()
|
||||||
|
+ Delta
|
||||||
|
- t()
|
||||||
|
+ Zebra
|
||||||
|
- t()
|
||||||
|
+ Mike
|
||||||
|
- t()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
+ OrderingFixtures$ClassOrderOuter
|
||||||
|
+ Alpha
|
||||||
|
- t()
|
||||||
|
+ Mike
|
||||||
|
- t()
|
||||||
|
+ Zebra
|
||||||
|
+ Charlie
|
||||||
|
- t()
|
||||||
|
+ Delta
|
||||||
|
- t()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
+ OrderingFixtures$MethodOrderInheritanceOuter
|
||||||
|
- alpha()
|
||||||
|
- bravo()
|
||||||
|
- charlie()
|
||||||
|
+ PlainNestedChild
|
||||||
|
- yankee()
|
||||||
|
- xray()
|
||||||
|
- zulu()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
+ OrderingFixtures$MethodOrderInheritanceOuter
|
||||||
|
- alpha()
|
||||||
|
- bravo()
|
||||||
|
- charlie()
|
||||||
|
+ PlainNestedChild
|
||||||
|
- xray()
|
||||||
|
- yankee()
|
||||||
|
- zulu()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
$ javac -cp <junit-jupiter-6.1.3 and its dependencies> src/broken/MethodOrdererDefaultDemo.java
|
||||||
|
Picked up JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36237 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
(compiled cleanly)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
$ javac -cp <junit-jupiter-5.14.4 and its dependencies> src/broken/MethodOrdererDefaultDemo.java
|
||||||
|
Picked up JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36237 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
src/broken/MethodOrdererDefaultDemo.java:22: error: cannot find symbol
|
||||||
|
@TestMethodOrder(MethodOrderer.Default.class) // <-- does not exist before JUnit 6.0
|
||||||
|
^
|
||||||
|
symbol: class Default
|
||||||
|
location: interface MethodOrderer
|
||||||
|
1 error
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
$ mvn test -Dtest=LoanDeskTest (no tag filter - everything runs)
|
||||||
|
[INFO] T E S T S
|
||||||
|
[INFO] -------------------------------------------------------
|
||||||
|
Picked up JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36237 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
[INFO] Running com.ankurm.tutorials.junit.organising.LoanDeskTest
|
||||||
|
[INFO] Running When no books are on loan
|
||||||
|
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.162 s -- in When no books are on loan
|
||||||
|
[INFO] Running When a book is ten days overdue
|
||||||
|
[INFO] Running and the member disputes the fee
|
||||||
|
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.004 s -- in and the member disputes the fee
|
||||||
|
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.075 s -- in When a book is ten days overdue
|
||||||
|
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.289 s -- in com.ankurm.tutorials.junit.organising.LoanDeskTest
|
||||||
|
[INFO]
|
||||||
|
[INFO] Results:
|
||||||
|
|
||||||
|
$ mvn test -Dgroups=fast -Dtest=LoanDeskTest
|
||||||
|
[INFO] T E S T S
|
||||||
|
[INFO] -------------------------------------------------------
|
||||||
|
Picked up JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36237 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
[INFO] Running com.ankurm.tutorials.junit.organising.LoanDeskTest
|
||||||
|
[INFO] Running When no books are on loan
|
||||||
|
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.149 s -- in When no books are on loan
|
||||||
|
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.201 s -- in com.ankurm.tutorials.junit.organising.LoanDeskTest
|
||||||
|
[INFO]
|
||||||
|
[INFO] Results:
|
||||||
|
|
||||||
|
$ mvn test -Dgroups=slow -Dtest=LoanDeskTest
|
||||||
|
[INFO] T E S T S
|
||||||
|
[INFO] -------------------------------------------------------
|
||||||
|
Picked up JAVA_TOOL_OPTIONS: -Djavax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts -Djavax.net.ssl.trustStoreType=JKS -Dhttps.proxyHost=127.0.0.1 -Dhttps.proxyPort=36237 -Dhttp.nonProxyHosts=localhost|127.0.0.1|::1|127.*|0.*|::|169.254.*|api.anthropic.com|api-staging.anthropic.com|api-pr-preview.anthropic.com|mcp-proxy.anthropic.com|mcp-proxy-staging.anthropic.com|registry.npmjs.org|jsr.io|npm.jsr.io|pypi.org|files.pythonhosted.org|index.crates.io|proxy.golang.org|host.docker.internal|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.*|192.168.*|100.64.0.0/10|*.svc.cluster.local|*.svc.cluster.local -Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes=
|
||||||
|
[INFO] Running com.ankurm.tutorials.junit.organising.LoanDeskTest
|
||||||
|
[INFO] Running When a book is ten days overdue
|
||||||
|
[INFO] Running and the member disputes the fee
|
||||||
|
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.016 s -- in and the member disputes the fee
|
||||||
|
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.170 s -- in When a book is ten days overdue
|
||||||
|
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.205 s -- in com.ankurm.tutorials.junit.organising.LoanDeskTest
|
||||||
|
[INFO]
|
||||||
|
[INFO] Results:
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?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>organising-demo</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<!-- override with -Djunit.version=5.14.4 to run the same suite on JUnit 5. MethodOrderer.Default
|
||||||
|
and ClassOrderer.Default do not exist on 5.14.4 - see src/broken/ and docs/output/06-*.txt -->
|
||||||
|
<junit.version>6.1.3</junit.version>
|
||||||
|
<!-- -Dgroups=fast / -DexcludedGroups=slow / -Dgroups="unit & !slow" from the CLI -->
|
||||||
|
<groups></groups>
|
||||||
|
<excludedGroups></excludedGroups>
|
||||||
|
</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>
|
||||||
|
<!-- used by RunHelper/OrderProbe to execute fixtures programmatically and inspect the real report tree -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.platform</groupId>
|
||||||
|
<artifactId>junit-platform-launcher</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.5.4</version>
|
||||||
|
<configuration>
|
||||||
|
<!-- -Dgroups=fast / -DexcludedGroups=slow / -Dgroups="unit & !slow" from the CLI -->
|
||||||
|
<groups>${groups}</groups>
|
||||||
|
<excludedGroups>${excludedGroups}</excludedGroups>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Compiles src/broken/MethodOrdererDefaultDemo.java against both JUnit versions and captures the
|
||||||
|
# REAL compiler output. MethodOrderer.Default was added in JUnit 6.0; this file is excluded from the
|
||||||
|
# normal build (it lives under src/broken, not src/test) precisely because it will not compile on 5.x.
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
mvn -q -B dependency:build-classpath -Dmdep.outputFile=/tmp/cp-6.1.3.txt
|
||||||
|
mvn -q -B dependency:build-classpath -Dmdep.outputFile=/tmp/cp-5.14.4.txt -Djunit.version=5.14.4
|
||||||
|
|
||||||
|
mkdir -p /tmp/broken-compile-out/6 /tmp/broken-compile-out/5
|
||||||
|
|
||||||
|
{
|
||||||
|
echo '$ javac -cp <junit-jupiter-6.1.3 and its dependencies> src/broken/MethodOrdererDefaultDemo.java'
|
||||||
|
javac -cp "$(cat /tmp/cp-6.1.3.txt)" -d /tmp/broken-compile-out/6 src/broken/MethodOrdererDefaultDemo.java
|
||||||
|
echo "(compiled cleanly)"
|
||||||
|
} > output/15-broken-compile-junit6.1.3.txt 2>&1
|
||||||
|
|
||||||
|
{
|
||||||
|
echo '$ javac -cp <junit-jupiter-5.14.4 and its dependencies> src/broken/MethodOrdererDefaultDemo.java'
|
||||||
|
javac -cp "$(cat /tmp/cp-5.14.4.txt)" -d /tmp/broken-compile-out/5 src/broken/MethodOrdererDefaultDemo.java
|
||||||
|
} > output/16-broken-compile-junit5.14.4.txt 2>&1
|
||||||
|
|
||||||
|
cat output/15-broken-compile-junit6.1.3.txt
|
||||||
|
echo
|
||||||
|
cat output/16-broken-compile-junit5.14.4.txt
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Captures the real Surefire console output of LoanDeskTest run with no filter, -Dgroups=fast, and
|
||||||
|
# -Dgroups=slow - this is what a reader actually sees typing these commands, not a paraphrase.
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
{
|
||||||
|
echo '$ mvn test -Dtest=LoanDeskTest (no tag filter - everything runs)'
|
||||||
|
mvn -B test -Dtest=LoanDeskTest 2>&1 | sed -n '/T E S T S/,/^\[INFO\] Results/p'
|
||||||
|
echo
|
||||||
|
echo '$ mvn test -Dgroups=fast -Dtest=LoanDeskTest'
|
||||||
|
mvn -B test -Dgroups=fast -Dtest=LoanDeskTest 2>&1 | sed -n '/T E S T S/,/^\[INFO\] Results/p'
|
||||||
|
echo
|
||||||
|
echo '$ mvn test -Dgroups=slow -Dtest=LoanDeskTest'
|
||||||
|
mvn -B test -Dgroups=slow -Dtest=LoanDeskTest 2>&1 | sed -n '/T E S T S/,/^\[INFO\] Results/p'
|
||||||
|
} > output/17-mvn-cli-tag-filter.txt
|
||||||
|
|
||||||
|
cat output/17-mvn-cli-tag-filter.txt
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates every file in output/ from scratch. Requires JDK 17+ and Maven on PATH.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
rm -rf output
|
||||||
|
mkdir -p output
|
||||||
|
|
||||||
|
echo "== 1/4: full suite on the default version (JUnit 6.1.3) =="
|
||||||
|
mvn -q -B test
|
||||||
|
|
||||||
|
echo "== 2/4: same suite on JUnit 5.14.4, keep only the two version-sensitive transcripts =="
|
||||||
|
mvn -q -B test -Djunit.version=5.14.4
|
||||||
|
cp output/12-nested-sibling-order-run1.txt output/12-nested-sibling-order-junit5.14.4.txt
|
||||||
|
cp output/14-method-order-inheritance.txt output/14-method-order-inheritance-junit5.14.4.txt
|
||||||
|
|
||||||
|
echo "== 3/4: back to the default version so output/ reflects JUnit 6.1.3 everywhere else =="
|
||||||
|
mvn -q -B test
|
||||||
|
|
||||||
|
echo "== 4/4: compiler-message transcripts for the broken/ demo, and a real mvn CLI tag-filter run =="
|
||||||
|
./scripts/broken-compile.sh
|
||||||
|
./scripts/mvn-cli-tag-filter.sh
|
||||||
|
|
||||||
|
echo "Done. See output/ for every transcript the article links to."
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising.broken;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excluded from the normal build (lives under src/broken, not src/test). Compiles on JUnit
|
||||||
|
* 6.1.3 and fails on JUnit 5.14.4: MethodOrderer.Default was added in JUnit 6.0 to let a @Nested
|
||||||
|
* class opt back out of an outer class's inherited @TestMethodOrder - see scripts/broken-compile.sh
|
||||||
|
* and output/15-broken-compile-6.1.3.txt / output/16-broken-compile-5.14.4.txt for the real
|
||||||
|
* compiler output on both versions.
|
||||||
|
*/
|
||||||
|
@TestMethodOrder(MethodOrderer.MethodName.class)
|
||||||
|
class MethodOrdererDefaultDemo {
|
||||||
|
@Test void charlie() {}
|
||||||
|
@Test void alpha() {}
|
||||||
|
@Test void bravo() {}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@TestMethodOrder(MethodOrderer.Default.class) // <-- does not exist before JUnit 6.0
|
||||||
|
class OptedBackOutOfInheritedOrdering {
|
||||||
|
@Test void zulu() {}
|
||||||
|
@Test void yankee() {}
|
||||||
|
@Test void xray() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||||
|
import org.junit.jupiter.api.DisplayNameGenerator;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Four small classes, same two tests each, one per generator - run through RunHelper.displayTree so
|
||||||
|
* the article can show the SAME test reported four different ways. None of these is itself picked
|
||||||
|
* up by Surefire (the name doesn't end in *Test/*Tests), which is deliberate: they are driven only
|
||||||
|
* through the Launcher from DisplayNameTest.
|
||||||
|
*/
|
||||||
|
final class DisplayNameFixtures {
|
||||||
|
private DisplayNameFixtures() {}
|
||||||
|
|
||||||
|
/** No annotation at all - this is what every reader sees by default. */
|
||||||
|
static class PlainDefault {
|
||||||
|
@Test
|
||||||
|
void total_price_is_zero_when_cart_is_empty() {}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addingAnItemIncreasesTheCount() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
|
||||||
|
static class Underscored {
|
||||||
|
@Test
|
||||||
|
void total_price_is_zero_when_cart_is_empty() {}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void adding_an_item_increases_the_count() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayName("A shopping cart")
|
||||||
|
@DisplayNameGeneration(DisplayNameGenerator.IndicativeSentences.class)
|
||||||
|
static class Sentences {
|
||||||
|
@Test
|
||||||
|
@DisplayName("has a total of zero when it is empty")
|
||||||
|
void total_price_is_zero_when_cart_is_empty() {}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("once an item has been added")
|
||||||
|
class OnceAnItemHasBeenAdded {
|
||||||
|
@Test
|
||||||
|
@DisplayName("has a total equal to that item's price")
|
||||||
|
void total_equals_item_price() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DisplayNameGeneration(SentenceCaseGenerator.class)
|
||||||
|
static class CustomSentenceCase {
|
||||||
|
@Test
|
||||||
|
void shouldReportZeroForAnEmptyCart() {}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectANegativeQuantity(TestInfo info) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used by the "no annotation, configured project-wide" scenario. */
|
||||||
|
static class ConfiguredByDefault {
|
||||||
|
@Test
|
||||||
|
void total_price_is_zero_when_cart_is_empty() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Runs the same two tests through four generators and pins the exact strings a reader will see. */
|
||||||
|
class DisplayNameTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultGeneratorUsesTheRawMethodSignature() {
|
||||||
|
List<String> tree = RunHelper.displayTree(DisplayNameFixtures.PlainDefault.class);
|
||||||
|
Events.write("01-default-generator.txt", tree);
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("total_price_is_zero_when_cart_is_empty()")));
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("addingAnItemIncreasesTheCount()")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void replaceUnderscoresTurnsSnakeCaseIntoSpaces() {
|
||||||
|
List<String> tree = RunHelper.displayTree(DisplayNameFixtures.Underscored.class);
|
||||||
|
Events.write("02-replace-underscores.txt", tree);
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("total price is zero when cart is empty")));
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("adding an item increases the count")));
|
||||||
|
// ReplaceUnderscores only touches underscores - camelCase in the method itself would stay camelCase
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void indicativeSentencesJoinsClassAndMethodIntoOneSentence() {
|
||||||
|
List<String> tree = RunHelper.displayTree(DisplayNameFixtures.Sentences.class);
|
||||||
|
Events.write("03-indicative-sentences.txt", tree);
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("A shopping cart")));
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("has a total of zero when it is empty")));
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("once an item has been added")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void customGeneratorAppliesTeamSpecificRules() {
|
||||||
|
List<String> tree = RunHelper.displayTree(DisplayNameFixtures.CustomSentenceCase.class);
|
||||||
|
Events.write("04-custom-generator.txt", tree);
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("Custom sentence case")));
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("Should report zero for an empty cart")));
|
||||||
|
assertTrue(tree.stream().anyMatch(s -> s.contains("Should reject a negative quantity(TestInfo)")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void defaultGeneratorCanBeSetProjectWideWithoutTouchingTheClass() {
|
||||||
|
List<String> plain = RunHelper.displayTree(DisplayNameFixtures.ConfiguredByDefault.class);
|
||||||
|
Events.write("05-no-config-param.txt", plain);
|
||||||
|
assertTrue(plain.stream().anyMatch(s -> s.contains("total_price_is_zero_when_cart_is_empty()")));
|
||||||
|
|
||||||
|
// same class, same jar, ONLY a configuration parameter changes - no annotation anywhere
|
||||||
|
List<String> configured = RunHelper.displayTreeWithConfig(
|
||||||
|
Map.of(org.junit.jupiter.api.DisplayNameGenerator.DEFAULT_GENERATOR_PROPERTY_NAME,
|
||||||
|
"org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores"),
|
||||||
|
DisplayNameFixtures.ConfiguredByDefault.class);
|
||||||
|
Events.write("06-configured-default-generator.txt", configured);
|
||||||
|
assertTrue(configured.stream().anyMatch(s -> s.contains("total price is zero when cart is empty")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Ordered event log shared by the fixtures; the tests assert on it and write it to output/. */
|
||||||
|
public final class Events {
|
||||||
|
private static final List<String> LOG = new ArrayList<>();
|
||||||
|
|
||||||
|
private Events() {}
|
||||||
|
|
||||||
|
public static synchronized void add(String s) { LOG.add(s); }
|
||||||
|
|
||||||
|
public static synchronized void clear() { LOG.clear(); }
|
||||||
|
|
||||||
|
public static synchronized List<String> snapshot() { return new ArrayList<>(LOG); }
|
||||||
|
|
||||||
|
/** Writes the lines to output/<name> relative to the module directory. */
|
||||||
|
public static void write(String name, List<String> lines) {
|
||||||
|
try {
|
||||||
|
Path dir = Path.of("output");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.write(dir.resolve(name), lines);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tiny production class under test - a library loan desk. Not the point of the article; it exists
|
||||||
|
* so the @Nested / @Tag examples assert on real behaviour instead of empty test bodies.
|
||||||
|
*/
|
||||||
|
class LoanDesk {
|
||||||
|
private static final int LOAN_PERIOD_DAYS = 7;
|
||||||
|
private static final int FINE_PER_DAY = 10;
|
||||||
|
|
||||||
|
private final Map<String, Integer> loanedDaysAgo = new HashMap<>();
|
||||||
|
private boolean feeFrozen;
|
||||||
|
private int frozenFee;
|
||||||
|
|
||||||
|
void loanBook(String isbn, int daysAgo) {
|
||||||
|
loanedDaysAgo.put(isbn, daysAgo);
|
||||||
|
}
|
||||||
|
|
||||||
|
void returnBook(String isbn) {
|
||||||
|
if (!loanedDaysAgo.containsKey(isbn)) {
|
||||||
|
throw new IllegalStateException("not on loan: " + isbn);
|
||||||
|
}
|
||||||
|
loanedDaysAgo.remove(isbn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fee owed right now, or the frozen amount if a dispute is in progress. */
|
||||||
|
int outstandingFee() {
|
||||||
|
if (feeFrozen) return frozenFee;
|
||||||
|
return loanedDaysAgo.values().stream()
|
||||||
|
.mapToInt(daysAgo -> Math.max(0, daysAgo - LOAN_PERIOD_DAYS) * FINE_PER_DAY)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
void disputeFee() {
|
||||||
|
frozenFee = outstandingFee();
|
||||||
|
feeFrozen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isFeeFrozen() {
|
||||||
|
return feeFrozen;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.DisplayNameGeneration;
|
||||||
|
import org.junit.jupiter.api.DisplayNameGenerator;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The worked example for the article: @DisplayNameGeneration + @Nested + @Tag used TOGETHER on one
|
||||||
|
* class, which the three source posts (4841, 5020, 5023) each show separately. Tags on an outer or
|
||||||
|
* middle class are inherited by every test underneath it - that is asserted here, not just claimed.
|
||||||
|
*/
|
||||||
|
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
|
||||||
|
@Tag("loan-desk")
|
||||||
|
class LoanDeskTest {
|
||||||
|
|
||||||
|
private LoanDesk desk;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void freshDesk() {
|
||||||
|
desk = new LoanDesk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@Tag("fast")
|
||||||
|
@DisplayName("When no books are on loan")
|
||||||
|
class When_no_books_are_on_loan {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void outstanding_fee_is_zero() {
|
||||||
|
assertEquals(0, desk.outstandingFee());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returning_an_unknown_book_is_rejected() {
|
||||||
|
assertThrows(IllegalStateException.class, () -> desk.returnBook("ISBN-404"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@Tag("slow")
|
||||||
|
@DisplayName("When a book is ten days overdue")
|
||||||
|
class When_a_book_is_ten_days_overdue {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void loanAnOverdueBook() {
|
||||||
|
desk.loanBook("ISBN-1", 10); // loan period is 7 days, so this one is 3 days late
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void outstanding_fee_accrues_at_ten_rupees_per_day_late() {
|
||||||
|
assertEquals(30, desk.outstandingFee());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returning_the_book_clears_the_loan() {
|
||||||
|
desk.returnBook("ISBN-1");
|
||||||
|
assertEquals(0, desk.outstandingFee());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("and the member disputes the fee")
|
||||||
|
class And_the_member_disputes_the_fee {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void raiseDispute() {
|
||||||
|
desk.disputeFee();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fee_is_frozen_at_the_disputed_amount() {
|
||||||
|
assertTrue(desk.isFeeFrozen());
|
||||||
|
assertEquals(30, desk.outstandingFee());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.ClassOrderer;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestClassOrder;
|
||||||
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixtures for the @Nested ordering scenario. Names are deliberately out of both declaration order
|
||||||
|
* and alphabetical order so an observed order actually means something.
|
||||||
|
*/
|
||||||
|
final class OrderingFixtures {
|
||||||
|
private OrderingFixtures() {}
|
||||||
|
|
||||||
|
/** No explicit orderer anywhere - whatever JUnit does by default for sibling @Nested classes. */
|
||||||
|
static class DefaultSiblingOrder {
|
||||||
|
@Nested
|
||||||
|
class Zebra {
|
||||||
|
@Test void t() {}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
class Mike {
|
||||||
|
@Test void t() {}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
class Alpha {
|
||||||
|
@Test void t() {}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
class Delta {
|
||||||
|
@Test void t() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @TestClassOrder on the outer class - does it reach a grandchild @Nested class two levels down? */
|
||||||
|
@TestClassOrder(ClassOrderer.ClassName.class)
|
||||||
|
static class ClassOrderOuter {
|
||||||
|
@Nested
|
||||||
|
class Zebra {
|
||||||
|
@Nested
|
||||||
|
class Delta {
|
||||||
|
@Test void t() {}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
class Charlie {
|
||||||
|
@Test void t() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
class Mike {
|
||||||
|
@Test void t() {}
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
class Alpha {
|
||||||
|
@Test void t() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @TestMethodOrder on the outer class. The outer methods are alphabetical by construction
|
||||||
|
* (MethodName orderer). Whether the plain @Nested child also runs alphabetically is exactly the
|
||||||
|
* JUnit 6.0 change ("inheritance of @TestMethodOrder by enclosed @Nested classes, recursively").
|
||||||
|
*/
|
||||||
|
@TestMethodOrder(MethodOrderer.MethodName.class)
|
||||||
|
static class MethodOrderInheritanceOuter {
|
||||||
|
@Test void charlie() {}
|
||||||
|
@Test void alpha() {}
|
||||||
|
@Test void bravo() {}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class PlainNestedChild {
|
||||||
|
@Test void zulu() {}
|
||||||
|
@Test void yankee() {}
|
||||||
|
@Test void xray() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class OrderingTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void siblingNestedClassesRunInAStableOrderAcrossRepeatedRuns() {
|
||||||
|
List<String> run1 = RunHelper.displayTree(OrderingFixtures.DefaultSiblingOrder.class);
|
||||||
|
List<String> run2 = RunHelper.displayTree(OrderingFixtures.DefaultSiblingOrder.class);
|
||||||
|
List<String> run3 = RunHelper.displayTree(OrderingFixtures.DefaultSiblingOrder.class);
|
||||||
|
Events.write("12-nested-sibling-order-run1.txt", run1);
|
||||||
|
Events.write("12-nested-sibling-order-run2.txt", run2);
|
||||||
|
Events.write("12-nested-sibling-order-run3.txt", run3);
|
||||||
|
assertEquals(run1, run2, "the engine must produce the same order every run, within one JUnit version");
|
||||||
|
assertEquals(run1, run3, "the engine must produce the same order every run, within one JUnit version");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testClassOrderReachesTwoLevelsOfNesting() {
|
||||||
|
List<String> tree = RunHelper.displayTree(OrderingFixtures.ClassOrderOuter.class);
|
||||||
|
Events.write("13-class-order-two-levels.txt", tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMethodOrderOnOuterClassAndWhatThePlainNestedChildDoes() {
|
||||||
|
List<String> tree = RunHelper.displayTree(OrderingFixtures.MethodOrderInheritanceOuter.class);
|
||||||
|
Events.write("14-method-order-inheritance.txt", tree);
|
||||||
|
// outer methods: MethodOrderer.MethodName always worked here, both 5.x and 6.x
|
||||||
|
List<String> outerOnly = tree.stream().filter(s -> s.startsWith(" - ")).toList();
|
||||||
|
assertEquals(List.of(" - alpha()", " - bravo()", " - charlie()"), outerOnly);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.platform.engine.TestExecutionResult;
|
||||||
|
import org.junit.platform.engine.discovery.ClassSelector;
|
||||||
|
import org.junit.platform.launcher.LauncherDiscoveryRequest;
|
||||||
|
import org.junit.platform.launcher.PostDiscoveryFilter;
|
||||||
|
import org.junit.platform.launcher.TestExecutionListener;
|
||||||
|
import org.junit.platform.launcher.TestIdentifier;
|
||||||
|
import org.junit.platform.launcher.TagFilter;
|
||||||
|
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
|
||||||
|
import org.junit.platform.launcher.core.LauncherFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs fixture classes through the real JUnit Platform Launcher - the same engine a build tool
|
||||||
|
* drives - and returns what actually happened. Every transcript in output/ comes from one of
|
||||||
|
* these two methods, never from a hand-typed example.
|
||||||
|
*/
|
||||||
|
final class RunHelper {
|
||||||
|
private RunHelper() {}
|
||||||
|
|
||||||
|
private static String describe(Throwable t) {
|
||||||
|
StringBuilder sb = new StringBuilder(" " + t.getClass().getSimpleName() + ": " + t.getMessage());
|
||||||
|
for (Throwable s : t.getSuppressed())
|
||||||
|
sb.append("\n suppressed ").append(s.getClass().getSimpleName()).append(": ").append(s.getMessage());
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pass/fail status lines per node, in completion order. Used for lifecycle- and failure-mode style checks. */
|
||||||
|
static List<String> run(Class<?>... classes) {
|
||||||
|
return run(Map.of(), (PostDiscoveryFilter[]) null, classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> run(Map<String, String> config, Class<?>... classes) {
|
||||||
|
return run(config, (PostDiscoveryFilter[]) null, classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> run(Map<String, String> config, PostDiscoveryFilter[] filters, Class<?>... classes) {
|
||||||
|
Events.clear();
|
||||||
|
var b = LauncherDiscoveryRequestBuilder.request();
|
||||||
|
List<ClassSelector> selectors = new ArrayList<>();
|
||||||
|
for (Class<?> c : classes) selectors.add(selectClass(c));
|
||||||
|
b.selectors(selectors);
|
||||||
|
config.forEach(b::configurationParameter);
|
||||||
|
if (filters != null) b.filters(filters);
|
||||||
|
LauncherDiscoveryRequest request = b.build();
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
TestExecutionListener l = new TestExecutionListener() {
|
||||||
|
@Override
|
||||||
|
public void executionSkipped(TestIdentifier id, String reason) {
|
||||||
|
results.add("SKIPPED " + id.getDisplayName() + " (" + reason + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void executionFinished(TestIdentifier id, TestExecutionResult r) {
|
||||||
|
boolean root = id.getParentId().isEmpty();
|
||||||
|
if (root && r.getStatus() == TestExecutionResult.Status.SUCCESSFUL) return;
|
||||||
|
String what = r.getThrowable().map(RunHelper::describe).orElse("");
|
||||||
|
results.add(String.format("%-10s %s%s", r.getStatus(), root ? "<engine>" : id.getDisplayName(), what));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
LauncherFactory.create().execute(request, l);
|
||||||
|
List<String> all = new ArrayList<>(Events.snapshot());
|
||||||
|
all.add("-- results --");
|
||||||
|
all.addAll(results);
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given classes and returns the real display-name tree, indented by depth, in the
|
||||||
|
* order each container/test was REPORTED STARTED by the engine - i.e. the order a reader
|
||||||
|
* would see scroll past in an IDE or a console run. This is what actually answers
|
||||||
|
* "what order does JUnit run @Nested classes in" - not a guess from reading the javadoc.
|
||||||
|
*/
|
||||||
|
static List<String> displayTree(Class<?>... classes) {
|
||||||
|
return displayTreeWithConfig(Map.of(), classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> displayTreeWithConfig(Map<String, String> config, Class<?>... classes) {
|
||||||
|
var b = LauncherDiscoveryRequestBuilder.request();
|
||||||
|
for (Class<?> c : classes) b.selectors(selectClass(c));
|
||||||
|
config.forEach(b::configurationParameter);
|
||||||
|
LauncherDiscoveryRequest request = b.build();
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
var launcher = LauncherFactory.create();
|
||||||
|
var plan = launcher.discover(request);
|
||||||
|
TestExecutionListener l = new TestExecutionListener() {
|
||||||
|
@Override
|
||||||
|
public void executionStarted(TestIdentifier id) {
|
||||||
|
if (id.getParentId().isEmpty()) return; // skip the synthetic engine root
|
||||||
|
int depth = 0;
|
||||||
|
var parent = plan.getParent(id);
|
||||||
|
while (parent.isPresent()) {
|
||||||
|
depth++;
|
||||||
|
parent = plan.getParent(parent.get());
|
||||||
|
}
|
||||||
|
lines.add(" ".repeat(depth - 1) + (id.isContainer() ? "+ " : "- ") + id.getDisplayName());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
launcher.execute(request, l);
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience: run with include/exclude tag filters, ANDed together exactly as Surefire's groups/excludedGroups would be. */
|
||||||
|
static List<String> runWithTags(List<String> include, List<String> exclude, Class<?>... classes) {
|
||||||
|
List<PostDiscoveryFilter> filters = new ArrayList<>();
|
||||||
|
if (!include.isEmpty()) filters.add(TagFilter.includeTags(include));
|
||||||
|
if (!exclude.isEmpty()) filters.add(TagFilter.excludeTags(exclude));
|
||||||
|
return run(Map.of(), filters.toArray(new PostDiscoveryFilter[0]), classes);
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.junit.jupiter.api.DisplayNameGenerator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom DisplayNameGenerator: strips a trailing "Test", splits camelCase/PascalCase into words,
|
||||||
|
* and lower-cases everything except the first letter - "shouldRejectExpiredCoupon" becomes
|
||||||
|
* "Should reject expired coupon". This is the shape you write when neither built-in generator
|
||||||
|
* matches your team's report style.
|
||||||
|
*/
|
||||||
|
public class SentenceCaseGenerator implements DisplayNameGenerator {
|
||||||
|
|
||||||
|
private static final Pattern WORD_BOUNDARY =
|
||||||
|
Pattern.compile("(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|_");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateDisplayNameForClass(Class<?> testClass) {
|
||||||
|
return toSentence(stripSuffix(testClass.getSimpleName(), "Test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateDisplayNameForNestedClass(java.util.List<Class<?>> enclosingInstanceTypes, Class<?> nestedClass) {
|
||||||
|
return toSentence(nestedClass.getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateDisplayNameForMethod(java.util.List<Class<?>> enclosingInstanceTypes, Class<?> testClass, Method testMethod) {
|
||||||
|
return toSentence(testMethod.getName()) + DisplayNameGenerator.parameterTypesAsString(testMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String stripSuffix(String s, String suffix) {
|
||||||
|
return s.endsWith(suffix) ? s.substring(0, s.length() - suffix.length()) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toSentence(String identifier) {
|
||||||
|
String[] words = WORD_BOUNDARY.split(identifier);
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < words.length; i++) {
|
||||||
|
if (words[i].isEmpty()) continue;
|
||||||
|
if (sb.length() > 0) sb.append(' ');
|
||||||
|
String w = words[i].toLowerCase(java.util.Locale.ROOT);
|
||||||
|
sb.append(sb.length() == 0 ? Character.toUpperCase(w.charAt(0)) + w.substring(1) : w);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
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.Tags;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Tag-filtering fixtures - driven only through RunHelper, never picked up directly by Surefire. */
|
||||||
|
final class TagFixtures {
|
||||||
|
private TagFixtures() {}
|
||||||
|
|
||||||
|
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Tag("unit")
|
||||||
|
@Tag("fast")
|
||||||
|
@interface FastUnitTest {}
|
||||||
|
|
||||||
|
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Tag("integration")
|
||||||
|
@Tag("slow")
|
||||||
|
@interface SlowIntegrationTest {}
|
||||||
|
|
||||||
|
@Tag("catalog")
|
||||||
|
static class CatalogServiceTest {
|
||||||
|
@FastUnitTest
|
||||||
|
@Test
|
||||||
|
void priceLookupIsCaseInsensitive() {
|
||||||
|
Events.add("catalog.priceLookupIsCaseInsensitive ran");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SlowIntegrationTest
|
||||||
|
@Test
|
||||||
|
void reindexingHitsTheRealSearchCluster() {
|
||||||
|
Events.add("catalog.reindexingHitsTheRealSearchCluster ran");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Tag("fast")
|
||||||
|
void emptyQueryReturnsNoResults() {
|
||||||
|
Events.add("catalog.emptyQueryReturnsNoResults ran");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Leading/trailing whitespace is silently TRIMMED - this is treated exactly as @Tag("fast"). */
|
||||||
|
@Tag(" fast ")
|
||||||
|
static class TrimmedTagClass {
|
||||||
|
@Test
|
||||||
|
void stillRuns() {
|
||||||
|
Events.add("TrimmedTagClass.stillRuns ran");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whitespace INSIDE a tag is not trimmable, so it is invalid - this one really is dropped with a warning. */
|
||||||
|
@Tag("fast lane")
|
||||||
|
static class InternalWhitespaceTagClass {
|
||||||
|
@Test
|
||||||
|
void stillRunsButTagIsDropped() {
|
||||||
|
Events.add("InternalWhitespaceTagClass.stillRunsButTagIsDropped ran");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Tags({@Tag("fast"), @Tag("")})
|
||||||
|
static class BlankTagClass {
|
||||||
|
@Test
|
||||||
|
void neverActuallyRuns() {
|
||||||
|
Events.add("BlankTagClass.neverActuallyRuns ran");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.ankurm.tutorials.junit.organising;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.platform.launcher.TagFilter;
|
||||||
|
|
||||||
|
class TagTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void includingFastRunsOnlyTheFastTests() {
|
||||||
|
List<String> log = RunHelper.runWithTags(List.of("fast"), List.of(), TagFixtures.CatalogServiceTest.class);
|
||||||
|
Events.write("07-tag-include-fast.txt", log);
|
||||||
|
assertTrue(log.contains("catalog.priceLookupIsCaseInsensitive ran"));
|
||||||
|
assertTrue(log.contains("catalog.emptyQueryReturnsNoResults ran"));
|
||||||
|
assertFalse(log.contains("catalog.reindexingHitsTheRealSearchCluster ran"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void excludingSlowSkipsTheIntegrationTest() {
|
||||||
|
List<String> log = RunHelper.runWithTags(List.of(), List.of("slow"), TagFixtures.CatalogServiceTest.class);
|
||||||
|
Events.write("08-tag-exclude-slow.txt", log);
|
||||||
|
assertTrue(log.contains("catalog.priceLookupIsCaseInsensitive ran"));
|
||||||
|
assertTrue(log.contains("catalog.emptyQueryReturnsNoResults ran"));
|
||||||
|
assertFalse(log.contains("catalog.reindexingHitsTheRealSearchCluster ran"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TagFilter.includeTags(List.of("a", "b")) is OR, not AND - a test matching EITHER expression is
|
||||||
|
* included. Passing two separate strings is NOT the same as requiring both tags; to require
|
||||||
|
* both you write one tag EXPRESSION with "&". Verified here, not assumed - the first run
|
||||||
|
* proves OR (everything with "catalog" OR "fast" gets in, including the slow one), the second
|
||||||
|
* proves "&" inside one expression narrows it back down.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void multipleIncludeTagsAreCombinedWithOrNotAnd() {
|
||||||
|
List<String> orResult = RunHelper.runWithTags(List.of("integration", "unit"), List.of(),
|
||||||
|
TagFixtures.CatalogServiceTest.class);
|
||||||
|
Events.write("09-tag-or-semantics.txt", orResult);
|
||||||
|
assertTrue(orResult.contains("catalog.priceLookupIsCaseInsensitive ran"), "has 'unit'");
|
||||||
|
assertTrue(orResult.contains("catalog.reindexingHitsTheRealSearchCluster ran"), "has 'integration'");
|
||||||
|
assertFalse(orResult.contains("catalog.emptyQueryReturnsNoResults ran"), "has neither tag");
|
||||||
|
|
||||||
|
List<String> andResult = RunHelper.run(java.util.Map.of(), new org.junit.platform.launcher.PostDiscoveryFilter[] {
|
||||||
|
TagFilter.includeTags("catalog & fast")
|
||||||
|
}, TagFixtures.CatalogServiceTest.class);
|
||||||
|
Events.write("09b-tag-and-expression.txt", andResult);
|
||||||
|
assertTrue(andResult.contains("catalog.priceLookupIsCaseInsensitive ran"), "catalog + fast");
|
||||||
|
assertTrue(andResult.contains("catalog.emptyQueryReturnsNoResults ran"), "catalog + fast");
|
||||||
|
assertFalse(andResult.contains("catalog.reindexingHitsTheRealSearchCluster ran"), "catalog but not fast");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leading/trailing whitespace in a @Tag is silently TRIMMED - no warning, and "-Dgroups=fast"
|
||||||
|
* matches it exactly as if you had written @Tag("fast"). Confirmed by actually filtering for it,
|
||||||
|
* not by reading the javadoc: a hand-typed stray space here is harmless.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void leadingAndTrailingWhitespaceInATagIsSilentlyTrimmed() {
|
||||||
|
List<String> fullRun = RunHelper.run(TagFixtures.TrimmedTagClass.class);
|
||||||
|
Events.write("10-trimmed-tag-still-runs.txt", fullRun);
|
||||||
|
assertTrue(fullRun.contains("TrimmedTagClass.stillRuns ran"));
|
||||||
|
|
||||||
|
List<String> filteredByFast = RunHelper.runWithTags(List.of("fast"), List.of(), TagFixtures.TrimmedTagClass.class);
|
||||||
|
Events.write("10b-trimmed-tag-matches-fast-filter.txt", filteredByFast);
|
||||||
|
assertTrue(filteredByFast.contains("TrimmedTagClass.stillRuns ran"),
|
||||||
|
"\" fast \" is trimmed to \"fast\" before matching, so the filter still finds it");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whitespace INSIDE a tag (not at the edges) is not trimmable - THAT is what actually triggers
|
||||||
|
* the "Invalid tag syntax... Tag will be ignored" warning. It is a non-critical discovery issue,
|
||||||
|
* not a build failure: the test body still runs as part of the full suite, but the bad tag is
|
||||||
|
* dropped, so a filter looking for it will never find this test again.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void internalWhitespaceMakesATagInvalidAndItIsDroppedWithAWarning() {
|
||||||
|
List<String> fullRun = RunHelper.run(TagFixtures.InternalWhitespaceTagClass.class);
|
||||||
|
Events.write("10c-invalid-tag-internal-whitespace.txt", fullRun);
|
||||||
|
assertTrue(fullRun.contains("InternalWhitespaceTagClass.stillRunsButTagIsDropped ran"), "the test body still runs");
|
||||||
|
|
||||||
|
// "fast lane" is not even a legal tag EXPRESSION (a bare space isn't an operator), which is
|
||||||
|
// further proof it could never have been registered as a real tag either.
|
||||||
|
List<String> filtered = RunHelper.runWithTags(List.of("fast"), List.of(), TagFixtures.InternalWhitespaceTagClass.class);
|
||||||
|
Events.write("10d-invalid-tag-never-matches-filter.txt", filtered);
|
||||||
|
assertFalse(filtered.contains("InternalWhitespaceTagClass.stillRunsButTagIsDropped ran"),
|
||||||
|
"the invalid tag was dropped at discovery, so no filter can ever match it");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aBlankTagFromATagsContainerIsAlsoJustDropped() {
|
||||||
|
List<String> log = RunHelper.run(TagFixtures.BlankTagClass.class);
|
||||||
|
Events.write("11-invalid-tag-blank.txt", log);
|
||||||
|
assertTrue(log.contains("BlankTagClass.neverActuallyRuns ran"), "the test body still runs");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user