From 84721c3e582cb78dd1bde857630a1fc15d609f99 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 15:53:40 +0000 Subject: [PATCH] Add repeated module: @RepeatedTest from JUnit 5 to JUnit 6 (6.1.3, JDK 25) --- repeated/README.md | 91 +++++++++++++ repeated/docs/output/00-full-test-run.txt | 22 ++++ repeated/docs/output/01-basic-repetition.txt | 7 + .../docs/output/02-custom-display-name.txt | 5 + .../output/03-bogus-placeholder-scratch.txt | 27 ++++ .../output/04-repetition-info-metadata.txt | 5 + repeated/docs/output/05-lifecycle-order.txt | 15 +++ repeated/docs/output/06-flaky-detected.txt | 47 +++++++ .../07-failure-threshold-skips-remaining.txt | 47 +++++++ repeated/pom.xml | 49 +++++++ repeated/scripts/run-all.sh | 121 ++++++++++++++++++ .../tutorials/junit/repeated/Capture.java | 27 ++++ .../junit/repeated/CustomNameTest.java | 40 ++++++ .../junit/repeated/LifecycleTest.java | 70 ++++++++++ .../junit/repeated/MetadataTest.java | 48 +++++++ .../junit/repeated/ReliabilityTest.java | 36 ++++++ 16 files changed, 657 insertions(+) create mode 100644 repeated/README.md create mode 100644 repeated/docs/output/00-full-test-run.txt create mode 100644 repeated/docs/output/01-basic-repetition.txt create mode 100644 repeated/docs/output/02-custom-display-name.txt create mode 100644 repeated/docs/output/03-bogus-placeholder-scratch.txt create mode 100644 repeated/docs/output/04-repetition-info-metadata.txt create mode 100644 repeated/docs/output/05-lifecycle-order.txt create mode 100644 repeated/docs/output/06-flaky-detected.txt create mode 100644 repeated/docs/output/07-failure-threshold-skips-remaining.txt create mode 100644 repeated/pom.xml create mode 100755 repeated/scripts/run-all.sh create mode 100644 repeated/src/test/java/com/ankurm/tutorials/junit/repeated/Capture.java create mode 100644 repeated/src/test/java/com/ankurm/tutorials/junit/repeated/CustomNameTest.java create mode 100644 repeated/src/test/java/com/ankurm/tutorials/junit/repeated/LifecycleTest.java create mode 100644 repeated/src/test/java/com/ankurm/tutorials/junit/repeated/MetadataTest.java create mode 100644 repeated/src/test/java/com/ankurm/tutorials/junit/repeated/ReliabilityTest.java diff --git a/repeated/README.md b/repeated/README.md new file mode 100644 index 0000000..db05001 --- /dev/null +++ b/repeated/README.md @@ -0,0 +1,91 @@ +# repeated + +Companion module for [JUnit @RepeatedTest (JUnit 5 → 6)](https://ankurm.com/) on ankurm.com. + +## What this is + +`@RepeatedTest` and `RepetitionInfo` are **byte-for-byte unchanged** between JUnit Jupiter 5.10.1 +and 6.1.3 — confirmed here with `javap` against the real jars, not assumed from a changelog. The +"JUnit 5 → 6" part of this module's story is almost entirely a version-number story: JUnit 6 +unified versioning across Platform, Jupiter and Vintage (they all now share one release number, +confirmed against the real `maven-metadata.xml` for all three artifacts) and raised the minimum +Java version to 17. Nothing about `@RepeatedTest` itself had to change for that. + +What *did* need fixing is what the previous version of this post got wrong or left out, each +verified against a real jar, a real sources jar, or a real run rather than against prose: + +- `{shortDisplayname}` is not a real placeholder. There are exactly three: + `DISPLAY_NAME_PLACEHOLDER`, `CURRENT_REPETITION_PLACEHOLDER`, `TOTAL_REPETITIONS_PLACEHOLDER` + (confirmed via `javap` on `RepeatedTest.class`). A `name()` pattern containing the bogus token + is accepted at compile time and left completely unresolved at run time — see + `docs/output/03-bogus-placeholder-scratch.txt`. +- `RepetitionInfo` carries **four** accessors, not two. `getFailureCount()` and + `getFailureThreshold()` have been on the interface since JUnit 5.10, the same release that + added `failureThreshold()` itself. +- `RepetitionInfo` can be injected into `@BeforeEach`/`@AfterEach`, not only into the + `@RepeatedTest` method. +- Once `failureThreshold` is reached, JUnit **skips** the remaining repetitions — it does not + merely "stop" them, and they are never invoked at all. See + `docs/output/07-failure-threshold-skips-remaining.txt` for the real console-tree output showing + `Failure threshold [2] exceeded` against repetitions 9 and 10. + +**Self-correction made before this module was finished:** the "flaky test detection" use case +needs a reproducible failure, not real timing-dependent flakiness, so both demo scratch classes +fail deterministically on fixed repetition numbers (4 and 8 of 10) rather than relying on actual +race conditions. Both were run for real, their output captured, and then deleted — they were +never meant to be part of the permanent green suite. + +## Versions + +| Dependency | Version | Verified against | +|---|---|---| +| JUnit Jupiter | 6.1.3 | `maven-metadata.xml` (`latest`/`release`), matching `junit-bom` and `junit-platform-launcher` — JUnit 6's unified versioning | +| AssertJ | 3.27.7 | `maven-metadata.xml`, `` | +| Maven Surefire | 3.6.0 | this module's own build | +| JDK | 25 (Temurin) | `java -version`, this module's own build | + +`@RepeatedTest`/`RepetitionInfo` class files are identical (via `javap`) across 5.10.1 and 6.1.3. +`getFailureCount()`/`getFailureThreshold()` are absent in 5.9.3's `RepeatedTest.class` and present +from 5.10.1 onward — the old post's "introduced in 5.10" claim for `failureThreshold` itself is +correct; it just never documented these two accessors. + +## Quickstart + +```bash +mvn test # 14 tests, 0 failures +``` + +## Test classes + +| Class | What it proves | Captured output | +|---|---|---| +| `ReliabilityTest` | The smallest `@RepeatedTest(5)`; real default display names resolved through injected `TestInfo` | `docs/output/01-basic-repetition.txt` | +| `CustomNameTest` | A custom `name()` pattern built from the three real placeholders | `docs/output/02-custom-display-name.txt` | +| `MetadataTest` | All four `RepetitionInfo` accessors, including the two the old post never covered | `docs/output/04-repetition-info-metadata.txt` | +| `LifecycleTest` | `@BeforeAll`/`@AfterAll` (once) vs `@BeforeEach`/`@AfterEach` (per repetition), plus `RepetitionInfo` injected into `@BeforeEach`/`@AfterEach` | `docs/output/05-lifecycle-order.txt` | + +## Captured output + +| File | Contents | +|---|---| +| `docs/output/00-full-test-run.txt` | `mvn test` — all 4 committed classes, 14 tests, 0 failures | +| `docs/output/01-basic-repetition.txt` | `ReliabilityTest` — real default display names | +| `docs/output/02-custom-display-name.txt` | `CustomNameTest` — real custom display names | +| `docs/output/03-bogus-placeholder-scratch.txt` | Scratch run proving `{shortDisplayname}` is not a real placeholder and resolves to literal text | +| `docs/output/04-repetition-info-metadata.txt` | `MetadataTest` — all four `RepetitionInfo` accessors across 3 repetitions | +| `docs/output/05-lifecycle-order.txt` | `LifecycleTest` — real call order and totals across 3 repetitions | +| `docs/output/06-flaky-detected.txt` | Scratch run: no `failureThreshold` set — all 10 repetitions run even though 2 fail | +| `docs/output/07-failure-threshold-skips-remaining.txt` | Scratch run: `failureThreshold = 2` — repetitions 9 and 10 skipped after the 2nd failure | + +`docs/output/03`, `06` and `07` come from scratch classes (`ScratchPlaceholderTest`, +`FlakyDetectionDemoTest`, `FailureThresholdStopsRemainingDemoTest`) that were written, run once +for real evidence, and then deleted — they are not part of the committed suite, which stays green +at 14/14. Re-run `scripts/run-all.sh` to see exactly how each transcript was produced. + +## What's sourced from documentation, not run here + +The JUnit 6.0.0 release notes facts quoted in the post (minimum Java 17, Vintage deprecated, +`junit-platform-runner`/`junit-platform-jfr` removal, `MethodOrderer.Alphanumeric` removal, +`@CsvSource` moving to FastCSV) are quoted from the official JUnit 6.0.0 release notes, not +exercised in this module — none of them touch `@RepeatedTest` or `RepetitionInfo` directly. +Everything else above was compiled, run, or pulled directly from a real jar in this sitting. diff --git a/repeated/docs/output/00-full-test-run.txt b/repeated/docs/output/00-full-test-run.txt new file mode 100644 index 0000000..defb689 --- /dev/null +++ b/repeated/docs/output/00-full-test-run.txt @@ -0,0 +1,22 @@ +# mvn test -- full committed suite (ReliabilityTest, CustomNameTest, LifecycleTest, MetadataTest) + +[INFO] ------------------------------------------------------- +[INFO] Running com.ankurm.tutorials.junit.repeated.ReliabilityTest +[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.275 s -- in com.ankurm.tutorials.junit.repeated.ReliabilityTest +[INFO] Running com.ankurm.tutorials.junit.repeated.CustomNameTest +[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.063 s -- in com.ankurm.tutorials.junit.repeated.CustomNameTest +[INFO] Running com.ankurm.tutorials.junit.repeated.LifecycleTest +[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.034 s -- in com.ankurm.tutorials.junit.repeated.LifecycleTest +[INFO] Running com.ankurm.tutorials.junit.repeated.MetadataTest +[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.027 s -- in com.ankurm.tutorials.junit.repeated.MetadataTest +[INFO] +[INFO] Results: +[INFO] +[INFO] Tests run: 14, Failures: 0, Errors: 0, Skipped: 0 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 2.788 s +[INFO] Finished at: 2026-10-08T21:23:02+05:30 +[INFO] ------------------------------------------------------------------------ diff --git a/repeated/docs/output/01-basic-repetition.txt b/repeated/docs/output/01-basic-repetition.txt new file mode 100644 index 0000000..00c43a8 --- /dev/null +++ b/repeated/docs/output/01-basic-repetition.txt @@ -0,0 +1,7 @@ +== TestInfo.getDisplayName() for each repetition == + +repetition 1 of 5 +repetition 2 of 5 +repetition 3 of 5 +repetition 4 of 5 +repetition 5 of 5 diff --git a/repeated/docs/output/02-custom-display-name.txt b/repeated/docs/output/02-custom-display-name.txt new file mode 100644 index 0000000..967fffb --- /dev/null +++ b/repeated/docs/output/02-custom-display-name.txt @@ -0,0 +1,5 @@ +== TestInfo.getDisplayName() with a custom name() pattern == + +Critical API Integration - Run 1/3 +Critical API Integration - Run 2/3 +Critical API Integration - Run 3/3 diff --git a/repeated/docs/output/03-bogus-placeholder-scratch.txt b/repeated/docs/output/03-bogus-placeholder-scratch.txt new file mode 100644 index 0000000..fc80274 --- /dev/null +++ b/repeated/docs/output/03-bogus-placeholder-scratch.txt @@ -0,0 +1,27 @@ +# Scratch run: {shortDisplayname} is not a real placeholder (ScratchPlaceholderTest, removed before commit) + + +Thanks for using JUnit! Support its development at https://junit.org/sponsoring + +╷ +├─ JUnit Platform Suite ✔ +├─ JUnit Jupiter ✔ +│ └─ ScratchPlaceholderTest ✔ +│ └─ bogusPlaceholderTest() ✔ +│ ├─ {shortDisplayname} test ✔ +│ └─ {shortDisplayname} test ✔ +└─ JUnit Vintage ✔ + +Test run finished after 204 ms +[ 5 containers found ] +[ 0 containers skipped ] +[ 5 containers started ] +[ 0 containers aborted ] +[ 5 containers successful ] +[ 0 containers failed ] +[ 2 tests found ] +[ 0 tests skipped ] +[ 2 tests started ] +[ 0 tests aborted ] +[ 2 tests successful ] +[ 0 tests failed ] diff --git a/repeated/docs/output/04-repetition-info-metadata.txt b/repeated/docs/output/04-repetition-info-metadata.txt new file mode 100644 index 0000000..b31c3cb --- /dev/null +++ b/repeated/docs/output/04-repetition-info-metadata.txt @@ -0,0 +1,5 @@ +== RepetitionInfo across all 3 repetitions == + +repetition 1 of 3 -- getFailureCount()=0 getFailureThreshold()=2 +repetition 2 of 3 -- getFailureCount()=0 getFailureThreshold()=2 +repetition 3 of 3 -- getFailureCount()=0 getFailureThreshold()=2 diff --git a/repeated/docs/output/05-lifecycle-order.txt b/repeated/docs/output/05-lifecycle-order.txt new file mode 100644 index 0000000..c5e6fc0 --- /dev/null +++ b/repeated/docs/output/05-lifecycle-order.txt @@ -0,0 +1,15 @@ +== Call order across 3 repetitions == + +@BeforeAll +@BeforeEach (repetition 1) +@RepeatedTest (repetition 1) +@AfterEach (repetition 1) +@BeforeEach (repetition 2) +@RepeatedTest (repetition 2) +@AfterEach (repetition 2) +@BeforeEach (repetition 3) +@RepeatedTest (repetition 3) +@AfterEach (repetition 3) +@AfterAll + +Totals -- @BeforeAll: 1, @AfterAll: 1, @BeforeEach: 3, @AfterEach: 3 diff --git a/repeated/docs/output/06-flaky-detected.txt b/repeated/docs/output/06-flaky-detected.txt new file mode 100644 index 0000000..d5008d2 --- /dev/null +++ b/repeated/docs/output/06-flaky-detected.txt @@ -0,0 +1,47 @@ +# Scratch run: @RepeatedTest with no failureThreshold -- all 10 repetitions still run even though 2 fail (FlakyDetectionDemoTest, removed before commit) + + +Thanks for using JUnit! Support its development at https://junit.org/sponsoring + +╷ +├─ JUnit Platform Suite ✔ +├─ JUnit Jupiter ✔ +│ └─ FlakyDetectionDemoTest ✔ +│ └─ intermittentRaceCondition(RepetitionInfo) ✔ +│ ├─ repetition 1 of 10 ✔ +│ ├─ repetition 2 of 10 ✔ +│ ├─ repetition 3 of 10 ✔ +│ ├─ repetition 4 of 10 ✘ simulated race condition reproduced on repetition 4 +│ ├─ repetition 5 of 10 ✔ +│ ├─ repetition 6 of 10 ✔ +│ ├─ repetition 7 of 10 ✔ +│ ├─ repetition 8 of 10 ✘ simulated race condition reproduced on repetition 8 +│ ├─ repetition 9 of 10 ✔ +│ └─ repetition 10 of 10 ✔ +└─ JUnit Vintage ✔ + +Failures (2): + JUnit Jupiter:FlakyDetectionDemoTest:intermittentRaceCondition(RepetitionInfo):repetition 4 of 10 + MethodSource [className = 'com.ankurm.tutorials.junit.repeated.FlakyDetectionDemoTest', methodName = 'intermittentRaceCondition', methodParameterTypes = 'org.junit.jupiter.api.RepetitionInfo'] + => org.opentest4j.AssertionFailedError: simulated race condition reproduced on repetition 4 + org.junit.jupiter.api.Assertions.fail(Assertions.java:142) + com.ankurm.tutorials.junit.repeated.FlakyDetectionDemoTest.intermittentRaceCondition(FlakyDetectionDemoTest.java:14) + JUnit Jupiter:FlakyDetectionDemoTest:intermittentRaceCondition(RepetitionInfo):repetition 8 of 10 + MethodSource [className = 'com.ankurm.tutorials.junit.repeated.FlakyDetectionDemoTest', methodName = 'intermittentRaceCondition', methodParameterTypes = 'org.junit.jupiter.api.RepetitionInfo'] + => org.opentest4j.AssertionFailedError: simulated race condition reproduced on repetition 8 + org.junit.jupiter.api.Assertions.fail(Assertions.java:142) + com.ankurm.tutorials.junit.repeated.FlakyDetectionDemoTest.intermittentRaceCondition(FlakyDetectionDemoTest.java:14) + +Test run finished after 281 ms +[ 5 containers found ] +[ 0 containers skipped ] +[ 5 containers started ] +[ 0 containers aborted ] +[ 5 containers successful ] +[ 0 containers failed ] +[ 10 tests found ] +[ 0 tests skipped ] +[ 10 tests started ] +[ 0 tests aborted ] +[ 8 tests successful ] +[ 2 tests failed ] diff --git a/repeated/docs/output/07-failure-threshold-skips-remaining.txt b/repeated/docs/output/07-failure-threshold-skips-remaining.txt new file mode 100644 index 0000000..d11e029 --- /dev/null +++ b/repeated/docs/output/07-failure-threshold-skips-remaining.txt @@ -0,0 +1,47 @@ +# Scratch run: @RepeatedTest(failureThreshold = 2) -- after the 2nd failure (repetition 8), repetitions 9 and 10 are SKIPPED, not invoked (FailureThresholdStopsRemainingDemoTest, removed before commit) + + +Thanks for using JUnit! Support its development at https://junit.org/sponsoring + +╷ +├─ JUnit Platform Suite ✔ +├─ JUnit Jupiter ✔ +│ └─ FailureThresholdStopsRemainingDemoTest ✔ +│ └─ intermittentRaceCondition(RepetitionInfo) ✔ +│ ├─ repetition 1 of 10 ✔ +│ ├─ repetition 2 of 10 ✔ +│ ├─ repetition 3 of 10 ✔ +│ ├─ repetition 4 of 10 ✘ simulated race condition reproduced on repetition 4 +│ ├─ repetition 5 of 10 ✔ +│ ├─ repetition 6 of 10 ✔ +│ ├─ repetition 7 of 10 ✔ +│ ├─ repetition 8 of 10 ✘ simulated race condition reproduced on repetition 8 +│ ├─ repetition 9 of 10 ↷ Failure threshold [2] exceeded +│ └─ repetition 10 of 10 ↷ Failure threshold [2] exceeded +└─ JUnit Vintage ✔ + +Failures (2): + JUnit Jupiter:FailureThresholdStopsRemainingDemoTest:intermittentRaceCondition(RepetitionInfo):repetition 4 of 10 + MethodSource [className = 'com.ankurm.tutorials.junit.repeated.FailureThresholdStopsRemainingDemoTest', methodName = 'intermittentRaceCondition', methodParameterTypes = 'org.junit.jupiter.api.RepetitionInfo'] + => org.opentest4j.AssertionFailedError: simulated race condition reproduced on repetition 4 + org.junit.jupiter.api.Assertions.fail(Assertions.java:142) + com.ankurm.tutorials.junit.repeated.FailureThresholdStopsRemainingDemoTest.intermittentRaceCondition(FailureThresholdStopsRemainingDemoTest.java:14) + JUnit Jupiter:FailureThresholdStopsRemainingDemoTest:intermittentRaceCondition(RepetitionInfo):repetition 8 of 10 + MethodSource [className = 'com.ankurm.tutorials.junit.repeated.FailureThresholdStopsRemainingDemoTest', methodName = 'intermittentRaceCondition', methodParameterTypes = 'org.junit.jupiter.api.RepetitionInfo'] + => org.opentest4j.AssertionFailedError: simulated race condition reproduced on repetition 8 + org.junit.jupiter.api.Assertions.fail(Assertions.java:142) + com.ankurm.tutorials.junit.repeated.FailureThresholdStopsRemainingDemoTest.intermittentRaceCondition(FailureThresholdStopsRemainingDemoTest.java:14) + +Test run finished after 292 ms +[ 5 containers found ] +[ 0 containers skipped ] +[ 5 containers started ] +[ 0 containers aborted ] +[ 5 containers successful ] +[ 0 containers failed ] +[ 10 tests found ] +[ 2 tests skipped ] +[ 8 tests started ] +[ 0 tests aborted ] +[ 6 tests successful ] +[ 2 tests failed ] diff --git a/repeated/pom.xml b/repeated/pom.xml new file mode 100644 index 0000000..a26cc09 --- /dev/null +++ b/repeated/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + com.ankurm + repeated + 1.0.0 + jar + repeated + JUnit @RepeatedTest from JUnit 5 to JUnit 6: RepetitionInfo, failureThreshold, and a + real flaky-test-detection use case, rebuilt against JUnit 6.1.3 on JDK 25. + + + 25 + UTF-8 + + 6.1.3 + 3.27.7 + + + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.6.0 + + + + diff --git a/repeated/scripts/run-all.sh b/repeated/scripts/run-all.sh new file mode 100755 index 0000000..504036c --- /dev/null +++ b/repeated/scripts/run-all.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Regenerates every file in docs/output/. +# +# docs/output/00, 01, 02, 04, 05 come straight from `mvn test` -- the four committed test +# classes write them via Capture.write() as a side effect of their own assertions. +# +# docs/output/03, 06, 07 come from three scratch classes that are deliberately NOT part of the +# committed suite (a bogus display-name placeholder, and two deterministic "flaky test" demos). +# This script recreates them in a throwaway directory, compiles and runs them against the +# JUnit Platform Console Standalone launcher, captures the real output, and cleans up -- so the +# committed suite stays green at 14/14 while every number in docs/output/ still traces back to a +# real run. +# +# Requires: JDK 17+ on PATH (JUnit 6 requires Java 17; this module was built on JDK 25), network +# access to download the console-standalone jar on first run. + +set -euo pipefail +cd "$(dirname "$0")/.." + +JUNIT_VERSION="6.1.3" +JAR_DIR="$(mktemp -d)" +STANDALONE_JAR="${JAR_DIR}/junit-platform-console-standalone-${JUNIT_VERSION}.jar" +trap 'rm -rf "${JAR_DIR}"' EXIT + +echo "== 1/3: committed suite (mvn test) ==" +mvn -q test +mvn test > "${JAR_DIR}/full-run.txt" 2>&1 || true +grep -v '^Picked up JAVA_TOOL_OPTIONS' "${JAR_DIR}/full-run.txt" | tail -20 > docs/output/00-full-test-run.txt +sed -i '1i# mvn test -- full committed suite (ReliabilityTest, CustomNameTest, LifecycleTest, MetadataTest)\n' docs/output/00-full-test-run.txt + +echo "== 2/3: downloading junit-platform-console-standalone ${JUNIT_VERSION} ==" +curl -fsSL -o "${STANDALONE_JAR}" \ + "https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/${JUNIT_VERSION}/junit-platform-console-standalone-${JUNIT_VERSION}.jar" + +echo "== 3/3: scratch classes (written, run, captured, discarded) ==" +SCRATCH_SRC="${JAR_DIR}/scratch-src" +SCRATCH_CLASSES="${JAR_DIR}/scratch-classes" +PKG_DIR="${SCRATCH_SRC}/com/ankurm/tutorials/junit/repeated" +mkdir -p "${PKG_DIR}" "${SCRATCH_CLASSES}" + +cat > "${PKG_DIR}/ScratchPlaceholderTest.java" <<'EOF' +package com.ankurm.tutorials.junit.repeated; + +import org.junit.jupiter.api.RepeatedTest; + +class ScratchPlaceholderTest { + + @RepeatedTest(value = 2, name = "{shortDisplayname} test") + void bogusPlaceholderTest() { + } +} +EOF + +cat > "${PKG_DIR}/FlakyDetectionDemoTest.java" <<'EOF' +package com.ankurm.tutorials.junit.repeated; + +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.RepetitionInfo; + +import static org.junit.jupiter.api.Assertions.fail; + +class FlakyDetectionDemoTest { + + @RepeatedTest(10) + void intermittentRaceCondition(RepetitionInfo info) { + int current = info.getCurrentRepetition(); + if (current == 4 || current == 8) { + fail("simulated race condition reproduced on repetition " + current); + } + } +} +EOF + +cat > "${PKG_DIR}/FailureThresholdStopsRemainingDemoTest.java" <<'EOF' +package com.ankurm.tutorials.junit.repeated; + +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.RepetitionInfo; + +import static org.junit.jupiter.api.Assertions.fail; + +class FailureThresholdStopsRemainingDemoTest { + + @RepeatedTest(value = 10, failureThreshold = 2) + void intermittentRaceCondition(RepetitionInfo info) { + int current = info.getCurrentRepetition(); + if (current == 4 || current == 8) { + fail("simulated race condition reproduced on repetition " + current); + } + } +} +EOF + +javac -cp "${STANDALONE_JAR}" -d "${SCRATCH_CLASSES}" "${PKG_DIR}"/*.java + +run_scratch() { + # These scratch runs are EXPECTED to report test failures (that's the point of 06 and 07), + # so capture output via command substitution with `|| true` rather than a raw pipe -- under + # `set -o pipefail` a piped non-zero exit from `java` would otherwise abort this script. + local class_name="$1" out_file="$2" header="$3" raw_out + raw_out="$(java -jar "${STANDALONE_JAR}" execute \ + -cp "${SCRATCH_CLASSES}" \ + --select-class="com.ankurm.tutorials.junit.repeated.${class_name}" \ + --details=tree --disable-ansi-colors 2>&1 || true)" + { + echo "${header}" + echo + printf '%s\n' "${raw_out}" | grep -v '^Picked up JAVA_TOOL_OPTIONS' + } > "docs/output/${out_file}" +} + +run_scratch "ScratchPlaceholderTest" "03-bogus-placeholder-scratch.txt" \ + "# Scratch run: {shortDisplayname} is not a real placeholder (ScratchPlaceholderTest, removed before commit)" + +run_scratch "FlakyDetectionDemoTest" "06-flaky-detected.txt" \ + "# Scratch run: @RepeatedTest with no failureThreshold -- all 10 repetitions still run even though 2 fail (FlakyDetectionDemoTest, removed before commit)" + +run_scratch "FailureThresholdStopsRemainingDemoTest" "07-failure-threshold-skips-remaining.txt" \ + "# Scratch run: @RepeatedTest(failureThreshold = 2) -- after the 2nd failure (repetition 8), repetitions 9 and 10 are SKIPPED, not invoked (FailureThresholdStopsRemainingDemoTest, removed before commit)" + +echo "Done -- docs/output/ regenerated." diff --git a/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/Capture.java b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/Capture.java new file mode 100644 index 0000000..18a72ff --- /dev/null +++ b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/Capture.java @@ -0,0 +1,27 @@ +package com.ankurm.tutorials.junit.repeated; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Writes a real line of test output to {@code docs/output/}, so every number quoted in + * the post traces back to a committed transcript rather than a remembered one. + */ +final class Capture { + + private Capture() { + } + + static void write(String fileName, String heading, String body) { + Path dir = Paths.get("docs", "output"); + try { + Files.createDirectories(dir); + Files.writeString(dir.resolve(fileName), "== " + heading + " ==\n\n" + body + "\n"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/CustomNameTest.java b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/CustomNameTest.java new file mode 100644 index 0000000..1b9180c --- /dev/null +++ b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/CustomNameTest.java @@ -0,0 +1,40 @@ +package com.ankurm.tutorials.junit.repeated; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.TestInfo; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A custom {@code name()} pattern built from {@link RepeatedTest#DISPLAY_NAME_PLACEHOLDER}, + * {@link RepeatedTest#CURRENT_REPETITION_PLACEHOLDER} and + * {@link RepeatedTest#TOTAL_REPETITIONS_PLACEHOLDER}. There is no fourth placeholder for a + * "short display name" - see {@code docs/output/03-bogus-placeholder-scratch.txt} for what + * actually happens if you try one anyway. + */ +class CustomNameTest { + + private static final List DISPLAY_NAMES = new CopyOnWriteArrayList<>(); + + @RepeatedTest(value = 3, name = "{displayName} - Run {currentRepetition}/{totalRepetitions}") + @DisplayName("Critical API Integration") + void customNameTest(TestInfo testInfo) { + assertThat(1 + 1).isEqualTo(2); + DISPLAY_NAMES.add(testInfo.getDisplayName()); + } + + @AfterAll + static void capture() { + assertThat(DISPLAY_NAMES).containsExactly( + "Critical API Integration - Run 1/3", + "Critical API Integration - Run 2/3", + "Critical API Integration - Run 3/3"); + Capture.write("02-custom-display-name.txt", "TestInfo.getDisplayName() with a custom name() pattern", + String.join("\n", DISPLAY_NAMES)); + } +} diff --git a/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/LifecycleTest.java b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/LifecycleTest.java new file mode 100644 index 0000000..70abf10 --- /dev/null +++ b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/LifecycleTest.java @@ -0,0 +1,70 @@ +package com.ankurm.tutorials.junit.repeated; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.RepetitionInfo; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Each repetition of a {@code @RepeatedTest} is a distinct test execution: {@code @BeforeEach} + * and {@code @AfterEach} fire for every one of them, while {@code @BeforeAll}/{@code @AfterAll} + * fire exactly once for the whole method. {@link RepetitionInfo} can also be injected into + * {@code @BeforeEach} directly - not just the test method itself - which the old version of this + * post did not mention. + */ +class LifecycleTest { + + private static final AtomicInteger BEFORE_ALL_CALLS = new AtomicInteger(); + private static final AtomicInteger AFTER_ALL_CALLS = new AtomicInteger(); + private static final AtomicInteger BEFORE_EACH_CALLS = new AtomicInteger(); + private static final AtomicInteger AFTER_EACH_CALLS = new AtomicInteger(); + private static final List ORDER = new CopyOnWriteArrayList<>(); + + @BeforeAll + static void onceBeforeAllRepetitions() { + BEFORE_ALL_CALLS.incrementAndGet(); + ORDER.add("@BeforeAll"); + } + + @BeforeEach + void beforeEveryRepetition(RepetitionInfo info) { + BEFORE_EACH_CALLS.incrementAndGet(); + ORDER.add("@BeforeEach (repetition " + info.getCurrentRepetition() + ")"); + } + + @RepeatedTest(3) + void everyRepetitionGetsFreshState(RepetitionInfo info) { + ORDER.add("@RepeatedTest (repetition " + info.getCurrentRepetition() + ")"); + } + + @AfterEach + void afterEveryRepetition(RepetitionInfo info) { + AFTER_EACH_CALLS.incrementAndGet(); + ORDER.add("@AfterEach (repetition " + info.getCurrentRepetition() + ")"); + } + + @AfterAll + static void onceAfterAllRepetitions() { + AFTER_ALL_CALLS.incrementAndGet(); + ORDER.add("@AfterAll"); + + assertThat(BEFORE_ALL_CALLS.get()).isEqualTo(1); + assertThat(AFTER_ALL_CALLS.get()).isEqualTo(1); + assertThat(BEFORE_EACH_CALLS.get()).isEqualTo(3); + assertThat(AFTER_EACH_CALLS.get()).isEqualTo(3); + + Capture.write("05-lifecycle-order.txt", "Call order across 3 repetitions", String.join("\n", ORDER) + + "\n\nTotals -- @BeforeAll: " + BEFORE_ALL_CALLS.get() + + ", @AfterAll: " + AFTER_ALL_CALLS.get() + + ", @BeforeEach: " + BEFORE_EACH_CALLS.get() + + ", @AfterEach: " + AFTER_EACH_CALLS.get()); + } +} diff --git a/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/MetadataTest.java b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/MetadataTest.java new file mode 100644 index 0000000..31505f5 --- /dev/null +++ b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/MetadataTest.java @@ -0,0 +1,48 @@ +package com.ankurm.tutorials.junit.repeated; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.RepetitionInfo; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * {@link RepetitionInfo} carries four pieces of state, not two. The old version of this post + * covered only {@link RepetitionInfo#getCurrentRepetition()} and + * {@link RepetitionInfo#getTotalRepetitions()}. The other two - + * {@link RepetitionInfo#getFailureCount()} and {@link RepetitionInfo#getFailureThreshold()} - + * have been on the interface since JUnit 5.10, the same release that added + * {@code failureThreshold()} itself, and let a test introspect its own threshold configuration + * without duplicating the number in two places. + */ +class MetadataTest { + + private static final List LINES = new CopyOnWriteArrayList<>(); + + @RepeatedTest(value = 3, failureThreshold = 2) + void testWithMetadata(RepetitionInfo info) { + int current = info.getCurrentRepetition(); + int total = info.getTotalRepetitions(); + assertThat(total).isEqualTo(3); + assertThat(current).isBetween(1, total); + + // No repetition here actually fails, so getFailureCount() stays 0 throughout - this + // demonstrates the accessor, not a threshold being crossed (see docs/output/07 for that). + assertThat(info.getFailureCount()).isZero(); + assertThat(info.getFailureThreshold()).isEqualTo(2); + + LINES.add("repetition " + current + " of " + total + + " -- getFailureCount()=" + info.getFailureCount() + + " getFailureThreshold()=" + info.getFailureThreshold()); + } + + @AfterAll + static void capture() { + assertThat(LINES).hasSize(3); + Capture.write("04-repetition-info-metadata.txt", "RepetitionInfo across all 3 repetitions", + String.join("\n", LINES)); + } +} diff --git a/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/ReliabilityTest.java b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/ReliabilityTest.java new file mode 100644 index 0000000..652a941 --- /dev/null +++ b/repeated/src/test/java/com/ankurm/tutorials/junit/repeated/ReliabilityTest.java @@ -0,0 +1,36 @@ +package com.ankurm.tutorials.junit.repeated; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.TestInfo; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The smallest possible {@code @RepeatedTest}: a fixed number, no {@code name()} override, no + * metadata parameter. JUnit's own default display name for each invocation is + * {@link RepeatedTest#SHORT_DISPLAY_NAME} - {@code "repetition {currentRepetition} of + * {totalRepetitions}"} - resolved here through the injected {@link TestInfo}, not assumed. + */ +class ReliabilityTest { + + private static final List DISPLAY_NAMES = new CopyOnWriteArrayList<>(); + + @RepeatedTest(5) + void simpleRepeatTest(TestInfo testInfo) { + assertThat(true).isTrue(); + DISPLAY_NAMES.add(testInfo.getDisplayName()); + } + + @AfterAll + static void capture() { + assertThat(DISPLAY_NAMES).containsExactly( + "repetition 1 of 5", "repetition 2 of 5", "repetition 3 of 5", + "repetition 4 of 5", "repetition 5 of 5"); + Capture.write("01-basic-repetition.txt", "TestInfo.getDisplayName() for each repetition", + String.join("\n", DISPLAY_NAMES)); + } +}