Add conditional module: JUnit conditional test execution on 6.1.3 (JDK 25)

Fixtures for the OS, JRE, system property, environment variable and EnabledIf
conditions plus a custom ExecutionCondition, run through the JUnit Platform
launcher (in-process and in child JVMs) to capture real skip reasons; skipped
versus aborted lifecycle; stacking, class-level skips and the deactivate switch;
the 2025 article's code compiled as published; and a javap comparison of the
condition package in 5.14.4 and 6.1.3.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01G8ikz8xdWuTP5yun8DZ1hk
This commit is contained in:
Claude
2026-10-11 10:24:30 +00:00
parent 84721c3e58
commit fd89649d8a
37 changed files with 1038 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
target/
*.class
+33
View File
@@ -0,0 +1,33 @@
# conditional
Companion code for [JUnit Conditional Test Execution (JUnit 5 → 6)](https://ankurm.com/mastering-junit-5-conditional-execution-build-smarter-environment-aware-tests/) on ankurm.com. The assumptions half of the story is in [JUnit 6 Assumptions](https://ankurm.com/junit-6-assumptions/).
The condition annotations (`@EnabledOnOs`, `@EnabledOnJre`, `@EnabledIfSystemProperty`, `@EnabledIfEnvironmentVariable`, `@EnabledIf`) and the `ExecutionCondition` extension point, run on JUnit Jupiter 6.1.3 and JDK 25. Every message and result quoted in the article is in [`output/`](output).
## How the evidence is produced
The *fixtures* in `src/test/java/.../fixtures/` are test classes that are meant to be skipped, aborted or to pass depending on the machine. They are excluded from Surefire. `ConditionalTest` starts the JUnit Platform on one fixture at a time (`Runner`), collects the result and the skip reason of each test, and writes the evidence file. For things a test cannot change in its own JVM (environment variables, `-D` flags) it starts a fresh JVM (`Child`) with the environment it wants.
| File | What it shows |
|---|---|
| `output/01-os-and-jre.txt` | The 2025 article's OS and JRE examples on this machine, with the real skip reasons |
| `output/02-architecture.txt` | `architectures` on `@EnabledOnOs` / `@DisabledOnOs` |
| `output/03-system-properties.txt` | `@EnabledIfSystemProperty` in three fresh JVMs |
| `output/04-environment-variables.txt` | `@EnabledIfEnvironmentVariable` with `CI` and `ENV` set and unset |
| `output/05-custom-condition.txt` | The article's `WeekendCondition`, and a reusable `@EnabledOnDays` condition with the day pinned |
| `output/06-skipped-versus-aborted.txt` | A condition-skipped test never starts; an assumption-aborted one has already run `@BeforeEach` |
| `output/07-stacking-and-switches.txt` | Several conditions on one test, `disabledReason`, `@EnabledIf("method")`, class-level skips, and switching conditions off |
| `output/08-old-code-vs-junit-6.txt` | The 2025 article's code blocks compiled exactly as published |
| `output/09-api-5-vs-6.txt` | `org.junit.jupiter.api.condition` in junit-jupiter-api 5.14.4 and 6.1.3 compared with `javap` |
## Run
```bash
scripts/run-all.sh # tests -> 01..07, then the compile and API comparison -> 08, 09
```
Needs JDK 17 or newer (built on 25) and Maven. Script 08/09 downloads two jars from Maven Central (it retries on rate limiting).
## Honest limits
Outputs 01, 02 and 07 describe the machine they ran on (Linux, amd64, JDK 25.0.4.1), so the OS, architecture and JRE conditions were only observed on that one combination. The first block of 05 depends on the day the tests ran. `src/old/` holds the 2025 article's code blocks verbatim; the copy under `fixtures/` adds a `package` line, and for `WeekendCondition` the `Test` import that the published block was missing.
+12
View File
@@ -0,0 +1,12 @@
This machine: os.name=Linux, os.arch=amd64, java.version=25.0.4.1
OS.current()=LINUX, JRE.currentVersion()=JAVA_25
OsConditionalTest
runEverywhereButSolaris() : PASSED
testUnixSocketBehavior() : PASSED
testWindowsRegistryAccess() : SKIPPED (Disabled on operating system: Linux)
JreConditionalTest
hideFromExperimentalVersions() : PASSED
testLegacyCompatibility() : SKIPPED (Disabled on JRE version: 25.0.4.1)
testLtsFeatureSet() : SKIPPED (Disabled on JRE version: 25.0.4.1)
+7
View File
@@ -0,0 +1,7 @@
This machine: os.name=Linux, os.arch=amd64, java.version=25.0.4.1
OS.current()=LINUX, JRE.currentVersion()=JAVA_25
ArchitectureConditionalTest
anythingButArm() : PASSED
onlyArmLinux() : SKIPPED (Disabled on operating system: Linux (amd64))
onlyIntelLinux() : PASSED
@@ -0,0 +1,13 @@
SystemPropertyTest in a fresh JVM, three times.
No -D flags:
runOnlyHighSeverityTests() : SKIPPED (System property [test.severity] does not exist)
testEnglishLocalization() : PASSED
-Dtest.severity=high -Duser.language=fr (the command line from the 2025 article):
runOnlyHighSeverityTests() : PASSED
testEnglishLocalization() : SKIPPED (System property [user.language] with value [fr] does not match regular expression [en])
-Dtest.severity=low -Duser.language=en:
runOnlyHighSeverityTests() : SKIPPED (System property [test.severity] with value [low] does not match regular expression [critical|high])
testEnglishLocalization() : PASSED
@@ -0,0 +1,13 @@
PipelineTest in a fresh JVM, three times, with the environment set by the test.
CI unset, ENV=DEV (a developer laptop):
integrationWithExternalAPI() : SKIPPED (Environment variable [ENV] with value [DEV] does not match regular expression [STAGING])
performanceBenchmark() : SKIPPED (Environment variable [CI] does not exist)
CI=true, ENV=STAGING (a pipeline):
integrationWithExternalAPI() : PASSED
performanceBenchmark() : PASSED
CI=false, ENV unset:
integrationWithExternalAPI() : SKIPPED (Environment variable [ENV] does not exist)
performanceBenchmark() : SKIPPED (Environment variable [CI] with value [false] does not match regular expression [true])
@@ -0,0 +1,19 @@
The WeekendCondition from the 2025 article, run today (2026-10-11, a SUNDAY):
weeklyBackupStabilityTest() : PASSED
The reusable version, @EnabledOnDays + DayOfWeekCondition, with the day pinned by the configuration parameter demo.today.
demo.today=SATURDAY
noAnnotationRunsAlways() : PASSED
wednesdayOnly() : SKIPPED (Today is SATURDAY, test runs on [WEDNESDAY])
weekendOnly() : PASSED
demo.today=WEDNESDAY
noAnnotationRunsAlways() : PASSED
wednesdayOnly() : PASSED
weekendOnly() : SKIPPED (Today is WEDNESDAY, test runs on [SATURDAY, SUNDAY])
demo.today=FRIDAY
noAnnotationRunsAlways() : PASSED
wednesdayOnly() : SKIPPED (Today is FRIDAY, test runs on [WEDNESDAY])
weekendOnly() : SKIPPED (Today is FRIDAY, test runs on [SATURDAY, SUNDAY])
@@ -0,0 +1,14 @@
Results
a_runs() : PASSED
b_skippedByAnnotation() : SKIPPED (Disabled on operating system: Linux)
c_abortedByAssumption() : ABORTED (Assumption failed: database is down)
Every callback and test body that ran, in order
@BeforeEach a_runs
body a_runs
@AfterEach a_runs
@BeforeEach c_abortedByAssumption
@AfterEach c_abortedByAssumption
The AssumptionTest from the 2025 article, unchanged
testDatabaseConnection() : ABORTED (Assumption failed: Aborting: Database is not reachable.)
@@ -0,0 +1,18 @@
This machine: os.name=Linux, os.arch=amd64, java.version=25.0.4.1
OS.current()=LINUX, JRE.currentVersion()=JAVA_25
StackingConditionalTest
bothConditionsFalse() : SKIPPED (Disabled on operating system: Linux)
methodConditionDefaultReason() : SKIPPED (@EnabledIf("alwaysFalse") evaluated to false)
methodConditionOwnReason() : SKIPPED (feature flag is off)
onlySecondConditionFalse() : SKIPPED (System property [never.set] does not exist)
withOwnReason() : SKIPPED (Disabled on operating system: Linux ==> Needs the Windows registry)
ClassLevelConditionalTest (@EnabledOnOs(WINDOWS) on the class, two test methods)
ClassLevelConditionalTest : CONTAINER SKIPPED (Disabled on operating system: Linux)
OsConditionalTest again, with the configuration parameter
junit.jupiter.conditions.deactivate=org.junit.jupiter.api.condition.*
runEverywhereButSolaris() : PASSED
testUnixSocketBehavior() : PASSED
testWindowsRegistryAccess() : PASSED
@@ -0,0 +1,12 @@
# The 2025 article's code blocks, compiled exactly as published, against JUnit Jupiter 6.1.3 on JDK 25.0.4.1
AssumptionTest: compiles
JreConditionalTest: compiles
OsConditionalTest: compiles
PipelineTest: compiles
SystemPropertyTest: compiles
WeekendCondition: DOES NOT COMPILE
src/old/WeekendCondition.java:19: error: cannot find symbol
symbol: class Test
location: class CustomConditionTest
1 error
+20
View File
@@ -0,0 +1,20 @@
# org/junit/jupiter/api/condition in junit-jupiter-api 5.14.4 (last JUnit 5 release on Maven Central) and 6.1.3
class file major version of EnabledOnOs in 5.14.4: 52 (52 = Java 8, 61 = Java 17)
class file major version of EnabledOnOs in 6.1.3: 61 (52 = Java 8, 61 = Java 17)
classes in the package: 39 in 5.14.4, 40 in 6.1.3
classes only in 6.1.3: org/junit/jupiter/api/condition/package-info.class
classes only in 5.14.4:
classes with an identical public and protected signature (javap): 37
classes whose signature differs: org.junit.jupiter.api.condition.AbstractJreRangeCondition org.junit.jupiter.api.condition.JRE
diff of javap org.junit.jupiter.api.condition.AbstractJreRangeCondition (5.14.4 -> 6.1.3):
4a5
> static {};
diff of javap org.junit.jupiter.api.condition.JRE (5.14.4 -> 6.1.3):
22a23,24
> public static final org.junit.jupiter.api.condition.JRE JAVA_27;
> public static final org.junit.jupiter.api.condition.JRE JAVA_28;
+59
View File
@@ -0,0 +1,59 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ankurm</groupId>
<artifactId>conditional</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>conditional</name>
<description>JUnit conditional test execution (@EnabledOn*, ExecutionCondition) from JUnit 5 to JUnit 6.1.3 on JDK 25.</description>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>6.1.3</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<!-- the tests start the JUnit Platform themselves (Runner), so the launcher API is on the compile classpath -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<!-- fixtures are test classes that are meant to be skipped or to fail: only Runner runs them -->
<excludes>
<exclude>**/fixtures/**</exclude>
</excludes>
<!-- the tests start child JVMs with this module's classpath, which needs a real java.class.path -->
<useManifestOnlyJar>false</useManifestOnlyJar>
</configuration>
</plugin>
</plugins>
</build>
</project>
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# Compiles the 2025 article's code blocks exactly as published (src/old/), and compares the
# org.junit.jupiter.api.condition package between the last JUnit 5 release and 6.1.3.
# Writes output/08-old-code-vs-junit-6.txt and output/09-api-5-vs-6.txt.
set -euo pipefail
cd "$(dirname "$0")/.."
mkdir -p output target/old target/cmp
mvn -q -B dependency:build-classpath -Dmdep.outputFile=target/cp.txt >/dev/null 2>&1
CP=$(cat target/cp.txt)
{
echo "# The 2025 article's code blocks, compiled exactly as published, against JUnit Jupiter 6.1.3 on JDK $(java -version 2>&1 | grep -o '"[^"]*"' | head -1 | tr -d '"')"
echo
for f in src/old/*.java; do
name=$(basename "$f" .java)
rm -rf target/old/out && mkdir -p target/old/out
if err=$(javac --release 25 -proc:none -cp "$CP" -d target/old/out "$f" 2>&1); then
echo "$name: compiles"
else
echo "$name: DOES NOT COMPILE"
echo "$err" | grep -E "error|symbol|location" | sed 's/^/ /'
fi
done
} > output/08-old-code-vs-junit-6.txt
cat output/08-old-code-vs-junit-6.txt
V5=5.14.4
V6=6.1.3
fetch() { # version -> target/cmp/api-<version>.jar, retrying while Maven Central answers 429
local v=$1 code
if unzip -tq target/cmp/api-$v.jar >/dev/null 2>&1; then return; fi
for try in 1 2 3 4 5 6; do
code=$(curl -s -o target/cmp/api-$v.jar -w '%{http_code}' \
"https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-api/$v/junit-jupiter-api-$v.jar" || echo 000)
[ "$code" = "200" ] && return
sleep $((try * 8))
done
echo "could not download junit-jupiter-api $v (HTTP $code)" >&2; exit 1
}
fetch $V5
fetch $V6
pkg=org/junit/jupiter/api/condition
{
echo "# $pkg in junit-jupiter-api $V5 (last JUnit 5 release on Maven Central) and $V6"
echo
for v in $V5 $V6; do
major=$(javap -v -cp target/cmp/api-$v.jar org.junit.jupiter.api.condition.EnabledOnOs 2>/dev/null | grep "major version" | awk '{print $3}')
echo "class file major version of EnabledOnOs in $v: $major (52 = Java 8, 61 = Java 17)"
done
echo
for v in $V5 $V6; do unzip -l target/cmp/api-$v.jar | grep "$pkg/" | awk '{print $4}' | sort > target/cmp/cls-$v.txt; done
echo "classes in the package: $(wc -l < target/cmp/cls-$V5.txt) in $V5, $(wc -l < target/cmp/cls-$V6.txt) in $V6"
echo "classes only in $V6: $(comm -13 target/cmp/cls-$V5.txt target/cmp/cls-$V6.txt | tr '\n' ' ')"
echo "classes only in $V5: $(comm -23 target/cmp/cls-$V5.txt target/cmp/cls-$V6.txt | tr '\n' ' ')"
echo
same=0; changed=""
for c in $(comm -12 target/cmp/cls-$V5.txt target/cmp/cls-$V6.txt | sed 's/\.class$//;s#/#.#g'); do
a=$({ javap -cp target/cmp/api-$V5.jar "$c" 2>&1 || true; } | grep -v Picked | md5sum || true)
b=$({ javap -cp target/cmp/api-$V6.jar "$c" 2>&1 || true; } | grep -v Picked | md5sum || true)
if [ "$a" = "$b" ]; then same=$((same+1)); else changed="$changed $c"; fi
done
echo "classes with an identical public and protected signature (javap): $same"
echo "classes whose signature differs:${changed:- none}"
for c in $changed; do
echo
echo "diff of javap $c ($V5 -> $V6):"
diff <(javap -cp target/cmp/api-$V5.jar "$c" 2>&1 | { grep -v Picked || true; }) <(javap -cp target/cmp/api-$V6.jar "$c" 2>&1 | { grep -v Picked || true; }) | sed 's/^/ /' || true
done
} > output/09-api-5-vs-6.txt
cat output/09-api-5-vs-6.txt
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Regenerates every file under output/: 01-07 from the tests, 08-09 from the old-snippet and API-diff script.
# No services needed. 01, 02 and 07 describe the machine they ran on; 05's first block depends on the day it runs.
set -euo pipefail
cd "$(dirname "$0")/.."
mvn -q -B test 2>&1 | grep -E "Tests run:|BUILD|FAIL|ERROR" || true
bash scripts/compile-old-snippets.sh >/dev/null
ls output
+13
View File
@@ -0,0 +1,13 @@
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assumptions.*;
class AssumptionTest {
@Test
void testDatabaseConnection() {
boolean isDbUp = false; // Mocking a down database
assumeTrue(isDbUp, "Aborting: Database is not reachable.");
System.out.println("This will not print because the assumption fails.");
}
}
@@ -0,0 +1,25 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
class JreConditionalTest {
@Test
@EnabledOnJre(JRE.JAVA_8)
void testLegacyCompatibility() {
System.out.println("Running on legacy Java 8 environment.");
}
@Test
@EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)
void testLtsFeatureSet() {
System.out.println("Running on a modern LTS version.");
}
@Test
@DisabledOnJre(JRE.OTHER)
void hideFromExperimentalVersions() {
System.out.println("Running on a stable, recognized JRE.");
}
}
@@ -0,0 +1,25 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
class OsConditionalTest {
@Test
@EnabledOnOs(OS.WINDOWS)
void testWindowsRegistryAccess() {
System.out.println("Executing Windows-specific logic.");
}
@Test
@EnabledOnOs({OS.LINUX, OS.MAC})
void testUnixSocketBehavior() {
System.out.println("Executing Unix-specific logic.");
}
@Test
@DisabledOnOs(OS.SOLARIS)
void runEverywhereButSolaris() {
System.out.println("Solaris is not supported for this module.");
}
}
+19
View File
@@ -0,0 +1,19 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
class PipelineTest {
@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
void performanceBenchmark() {
System.out.println("Benchmark running on dedicated CI worker.");
}
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "STAGING")
void integrationWithExternalAPI() {
System.out.println("Connecting to Staging API...");
}
}
@@ -0,0 +1,19 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
class SystemPropertyTest {
@Test
@EnabledIfSystemProperty(named = "test.severity", matches = "critical|high")
void runOnlyHighSeverityTests() {
System.out.println("Running a high-priority test case.");
}
@Test
@EnabledIfSystemProperty(named = "user.language", matches = "en")
void testEnglishLocalization() {
System.out.println("Testing English locale specific logic.");
}
}
+25
View File
@@ -0,0 +1,25 @@
import org.junit.jupiter.api.extension.*;
import java.time.DayOfWeek;
import java.time.LocalDate;
public class WeekendCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
DayOfWeek today = LocalDate.now().getDayOfWeek();
if (today == DayOfWeek.SATURDAY || today == DayOfWeek.SUNDAY) {
return ConditionEvaluationResult.enabled("It is the weekend!");
}
return ConditionEvaluationResult.disabled("Skipping: Today is " + today);
}
}
// Usage in your test class
class CustomConditionTest {
@Test
@ExtendWith(WeekendCondition.class)
void weeklyBackupStabilityTest() {
System.out.println("Running heavy weekend-only backup test.");
}
}
@@ -0,0 +1,39 @@
package com.ankurm.tutorials.junit.conditional;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Runs Runner in a fresh JVM with a chosen environment and -D flags, and returns its result lines. */
final class Child {
private Child() {
}
static List<String> run(String fixture, Map<String, String> env, Set<String> unsetEnv, Map<String, String> sysProps, String... configuration) {
try {
List<String> cmd = new ArrayList<>();
cmd.add(Path.of(System.getProperty("java.home"), "bin", "java").toString());
sysProps.forEach((k, v) -> cmd.add("-D" + k + "=" + v));
cmd.add("-cp");
cmd.add(System.getProperty("java.class.path"));
cmd.add(Runner.class.getName());
cmd.add(fixture);
cmd.addAll(List.of(configuration));
ProcessBuilder pb = new ProcessBuilder(cmd).redirectErrorStream(true);
unsetEnv.forEach(pb.environment()::remove);
pb.environment().putAll(env);
Process p = pb.start();
String out = new String(p.getInputStream().readAllBytes());
if (p.waitFor() != 0) {
throw new IllegalStateException("child JVM failed:\n" + out);
}
return out.lines().filter(l -> l.startsWith("@@ ")).map(l -> l.substring(3)).toList();
} catch (IOException | InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,144 @@
package com.ankurm.tutorials.junit.conditional;
import com.ankurm.tutorials.junit.conditional.Runner.Result;
import com.ankurm.tutorials.junit.conditional.fixtures.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.OS;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
class ConditionalTest {
private static final Map<String, String> NONE = Map.of();
private static String lines(List<?> items) {
return items.stream().map(i -> " " + i).collect(Collectors.joining("\n"));
}
private static String machine() {
return "This machine: os.name=" + System.getProperty("os.name") + ", os.arch=" + System.getProperty("os.arch")
+ ", java.version=" + System.getProperty("java.version")
+ "\n OS.current()=" + OS.current() + ", JRE.currentVersion()=" + JRE.currentVersion();
}
private static Result only(List<Result> results, String method) {
return results.stream().filter(r -> r.name().equals(method + "()")).findFirst().orElseThrow();
}
// ------------------------------------------------------------------ 1. operating system and JRE
@Test
void operatingSystemAndJreConditions() {
List<Result> os = Runner.run("OsConditionalTest", NONE);
List<Result> jre = Runner.run("JreConditionalTest", NONE);
Transcript.write("01-os-and-jre.txt", machine() + "\n\nOsConditionalTest\n" + lines(os) + "\n\nJreConditionalTest\n" + lines(jre));
boolean windows = OS.current() == OS.WINDOWS;
assertThat(only(os, "testWindowsRegistryAccess").state()).isEqualTo(windows ? "PASSED" : "SKIPPED");
assertThat(only(os, "runEverywhereButSolaris").state()).isEqualTo("PASSED");
assertThat(only(jre, "testLegacyCompatibility").state()).isEqualTo("SKIPPED");
}
// ------------------------------------------------------------------ 2. architecture
@Test
void architectureAttribute() {
List<Result> results = Runner.run("ArchitectureConditionalTest", NONE);
Transcript.write("02-architecture.txt", machine() + "\n\nArchitectureConditionalTest\n" + lines(results));
assertThat(results).hasSize(3);
}
// ------------------------------------------------------------------ 3. system properties
@Test
void systemPropertiesAreMatchedWithRegularExpressions() {
var none = Child.run("SystemPropertyTest", Map.of(), Set.of(), Map.of());
var fr = Child.run("SystemPropertyTest", Map.of(), Set.of(), Map.of("test.severity", "high", "user.language", "fr"));
var low = Child.run("SystemPropertyTest", Map.of(), Set.of(), Map.of("test.severity", "low", "user.language", "en"));
Transcript.write("03-system-properties.txt",
"SystemPropertyTest in a fresh JVM, three times.\n\n"
+ "No -D flags:\n" + lines(none)
+ "\n\n-Dtest.severity=high -Duser.language=fr (the command line from the 2025 article):\n" + lines(fr)
+ "\n\n-Dtest.severity=low -Duser.language=en:\n" + lines(low));
assertThat(fr).anyMatch(l -> l.startsWith("runOnlyHighSeverityTests() : PASSED"));
assertThat(low).anyMatch(l -> l.startsWith("runOnlyHighSeverityTests() : SKIPPED"));
}
// ------------------------------------------------------------------ 4. environment variables
@Test
void environmentVariables() {
var local = Child.run("PipelineTest", Map.of("ENV", "DEV"), Set.of("CI"), Map.of());
var ci = Child.run("PipelineTest", Map.of("CI", "true", "ENV", "STAGING"), Set.of(), Map.of());
var ciFalse = Child.run("PipelineTest", Map.of("CI", "false"), Set.of("ENV"), Map.of());
Transcript.write("04-environment-variables.txt",
"PipelineTest in a fresh JVM, three times, with the environment set by the test.\n\n"
+ "CI unset, ENV=DEV (a developer laptop):\n" + lines(local)
+ "\n\nCI=true, ENV=STAGING (a pipeline):\n" + lines(ci)
+ "\n\nCI=false, ENV unset:\n" + lines(ciFalse));
assertThat(ci).allMatch(l -> l.contains(": PASSED"));
assertThat(local).allMatch(l -> l.contains(": SKIPPED"));
}
// ------------------------------------------------------------------ 5. custom conditions
@Test
void customExecutionConditions() {
LocalDate today = LocalDate.now();
List<Result> weekend = Runner.run("CustomConditionTest", NONE);
List<Result> saturday = Runner.run("DaysConditionalTest", Map.of("demo.today", "SATURDAY"));
List<Result> wednesday = Runner.run("DaysConditionalTest", Map.of("demo.today", "WEDNESDAY"));
List<Result> friday = Runner.run("DaysConditionalTest", Map.of("demo.today", "FRIDAY"));
Transcript.write("05-custom-condition.txt",
"The WeekendCondition from the 2025 article, run today (" + today + ", a " + today.getDayOfWeek() + "):\n" + lines(weekend)
+ "\n\nThe reusable version, @EnabledOnDays + DayOfWeekCondition, with the day pinned by the configuration parameter demo.today.\n\n"
+ "demo.today=SATURDAY\n" + lines(saturday)
+ "\n\ndemo.today=WEDNESDAY\n" + lines(wednesday)
+ "\n\ndemo.today=FRIDAY\n" + lines(friday));
boolean weekendToday = today.getDayOfWeek() == DayOfWeek.SATURDAY || today.getDayOfWeek() == DayOfWeek.SUNDAY;
assertThat(weekend.get(0).state()).isEqualTo(weekendToday ? "PASSED" : "SKIPPED");
assertThat(only(saturday, "weekendOnly").state()).isEqualTo("PASSED");
assertThat(only(saturday, "wednesdayOnly").state()).isEqualTo("SKIPPED");
assertThat(only(wednesday, "wednesdayOnly").state()).isEqualTo("PASSED");
assertThat(only(friday, "noAnnotationRunsAlways").state()).isEqualTo("PASSED");
}
// ------------------------------------------------------------------ 6. static conditions against assumptions
@Test
void aSkippedTestNeverStartsButAnAbortedOneHas() {
LifecycleConditionalTest.EVENTS.clear();
List<Result> results = Runner.run("LifecycleConditionalTest", NONE);
List<Result> assumption = Runner.run("AssumptionTest", NONE);
Transcript.write("06-skipped-versus-aborted.txt",
"Results\n" + lines(results) + "\n\nEvery callback and test body that ran, in order\n" + lines(LifecycleConditionalTest.EVENTS)
+ "\n\nThe AssumptionTest from the 2025 article, unchanged\n" + lines(assumption));
assertThat(LifecycleConditionalTest.EVENTS).noneMatch(e -> e.contains("b_skippedByAnnotation"));
assertThat(LifecycleConditionalTest.EVENTS).contains("@BeforeEach c_abortedByAssumption", "@AfterEach c_abortedByAssumption");
assertThat(only(results, "c_abortedByAssumption").state()).isEqualTo("ABORTED");
assertThat(assumption.get(0).reason()).isEqualTo("Assumption failed: Aborting: Database is not reachable.");
}
// ------------------------------------------------------------------ 7. stacking, reasons, class level, switching off
@Test
void stackedConditionsReasonsClassLevelAndDeactivation() {
List<Result> stacked = Runner.run("StackingConditionalTest", NONE);
List<Result> classLevel = Runner.run("ClassLevelConditionalTest", NONE);
List<Result> deactivated = Runner.run("OsConditionalTest", Map.of("junit.jupiter.conditions.deactivate", "org.junit.jupiter.api.condition.*"));
Transcript.write("07-stacking-and-switches.txt",
machine() + "\n\nStackingConditionalTest\n" + lines(stacked)
+ "\n\nClassLevelConditionalTest (@EnabledOnOs(WINDOWS) on the class, two test methods)\n" + lines(classLevel)
+ "\n\nOsConditionalTest again, with the configuration parameter\n junit.jupiter.conditions.deactivate=org.junit.jupiter.api.condition.*\n" + lines(deactivated));
assertThat(only(stacked, "methodConditionOwnReason").reason()).isEqualTo("feature flag is off");
assertThat(deactivated).allMatch(r -> r.state().equals("PASSED"));
}
}
@@ -0,0 +1,77 @@
package com.ankurm.tutorials.junit.conditional;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
/**
* Starts the JUnit Platform on one fixture class and reports what happened to each test, including the skip reason.
* Used in-process by the tests, and as a main class for child JVMs so a test can vary env vars and -D flags.
*/
public final class Runner {
public record Result(String name, String state, String reason) {
@Override
public String toString() {
return name + " : " + state + (reason.isEmpty() ? "" : " (" + reason + ")");
}
}
public static final String FIXTURES = "com.ankurm.tutorials.junit.conditional.fixtures.";
public static List<Result> run(String fixtureSimpleName, Map<String, String> configuration) {
List<Result> results = new ArrayList<>();
Class<?> fixture;
try {
fixture = Class.forName(FIXTURES + fixtureSimpleName);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
var request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectClass(fixture))
.configurationParameters(configuration)
.build();
Launcher launcher = LauncherFactory.create();
launcher.execute(request, new TestExecutionListener() {
@Override
public void executionSkipped(TestIdentifier id, String reason) {
results.add(new Result(id.getDisplayName(), id.isTest() ? "SKIPPED" : "CONTAINER SKIPPED", reason));
}
@Override
public void executionFinished(TestIdentifier id, TestExecutionResult r) {
if (!id.isTest()) {
return;
}
String state = switch (r.getStatus()) {
case SUCCESSFUL -> "PASSED";
case ABORTED -> "ABORTED";
case FAILED -> "FAILED";
};
results.add(new Result(id.getDisplayName(), state, r.getThrowable().map(Throwable::getMessage).orElse("")));
}
});
results.sort((a, b) -> a.name().compareTo(b.name()));
return results;
}
/** java -cp ... Runner FixtureSimpleName [key=value ...] prints one "@@ line" per result. */
public static void main(String[] args) throws Exception {
Map<String, String> configuration = new HashMap<>();
for (int i = 1; i < args.length; i++) {
String[] kv = args[i].split("=", 2);
configuration.put(kv[0], kv[1]);
}
run(args[0], configuration).forEach(r -> System.out.println("@@ " + r));
}
}
@@ -0,0 +1,22 @@
package com.ankurm.tutorials.junit.conditional;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/** Writes the evidence files that the article quotes. */
final class Transcript {
private Transcript() {
}
static void write(String name, String text) {
try {
Path dir = Path.of("output");
Files.createDirectories(dir);
Files.writeString(dir.resolve(name), text.endsWith("\n") ? text : text + "\n");
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,22 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
class ArchitectureConditionalTest {
@Test
@EnabledOnOs(value = OS.LINUX, architectures = "aarch64")
void onlyArmLinux() {
}
@Test
@EnabledOnOs(value = OS.LINUX, architectures = {"x86_64", "amd64"})
void onlyIntelLinux() {
}
@Test
@DisabledOnOs(architectures = "aarch64")
void anythingButArm() {
}
}
@@ -0,0 +1,15 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assumptions.*;
class AssumptionTest {
@Test
void testDatabaseConnection() {
boolean isDbUp = false; // Mocking a down database
assumeTrue(isDbUp, "Aborting: Database is not reachable.");
System.out.println("This will not print because the assumption fails.");
}
}
@@ -0,0 +1,17 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
@EnabledOnOs(OS.WINDOWS)
class ClassLevelConditionalTest {
@Test
void first() {
}
@Test
void second() {
}
}
@@ -0,0 +1,32 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.support.AnnotationSupport;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.Arrays;
/**
* The reusable version of WeekendCondition: reads its days from an annotation, and takes "today" from the
* configuration parameter demo.today when set, so a test can pretend it is any day of the week.
*/
public class DayOfWeekCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
var annotation = context.getElement().flatMap(e -> AnnotationSupport.findAnnotation(e, EnabledOnDays.class));
if (annotation.isEmpty()) {
return ConditionEvaluationResult.enabled("@EnabledOnDays is not present");
}
DayOfWeek today = context.getConfigurationParameter("demo.today")
.map(DayOfWeek::valueOf)
.orElseGet(() -> LocalDate.now().getDayOfWeek());
DayOfWeek[] days = annotation.get().value();
return Arrays.asList(days).contains(today)
? ConditionEvaluationResult.enabled("Today is " + today)
: ConditionEvaluationResult.disabled("Today is " + today + ", test runs on " + Arrays.toString(days));
}
}
@@ -0,0 +1,22 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import java.time.DayOfWeek;
class DaysConditionalTest {
@Test
@EnabledOnDays({DayOfWeek.SATURDAY, DayOfWeek.SUNDAY})
void weekendOnly() {
}
@Test
@EnabledOnDays(DayOfWeek.WEDNESDAY)
void wednesdayOnly() {
}
@Test
void noAnnotationRunsAlways() {
}
}
@@ -0,0 +1,14 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.extension.ExtendWith;
import java.lang.annotation.*;
import java.time.DayOfWeek;
/** Runs the test only on the listed days. The condition behind it is {@link DayOfWeekCondition}. */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(DayOfWeekCondition.class)
public @interface EnabledOnDays {
DayOfWeek[] value();
}
@@ -0,0 +1,27 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
class JreConditionalTest {
@Test
@EnabledOnJre(JRE.JAVA_8)
void testLegacyCompatibility() {
System.out.println("Running on legacy Java 8 environment.");
}
@Test
@EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21)
void testLtsFeatureSet() {
System.out.println("Running on a modern LTS version.");
}
@Test
@DisabledOnJre(JRE.OTHER)
void hideFromExperimentalVersions() {
System.out.println("Running on a stable, recognized JRE.");
}
}
@@ -0,0 +1,44 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
/** Records what runs around a test that is skipped by an annotation and a test that is aborted by an assumption. */
@TestMethodOrder(MethodOrderer.MethodName.class)
public class LifecycleConditionalTest {
public static final List<String> EVENTS = new CopyOnWriteArrayList<>();
@BeforeEach
void before(TestInfo info) {
EVENTS.add("@BeforeEach " + info.getTestMethod().orElseThrow().getName());
}
@AfterEach
void after(TestInfo info) {
EVENTS.add("@AfterEach " + info.getTestMethod().orElseThrow().getName());
}
@Test
void a_runs() {
EVENTS.add("body a_runs");
}
@Test
@EnabledOnOs(OS.WINDOWS)
void b_skippedByAnnotation() {
EVENTS.add("body b_skippedByAnnotation");
}
@Test
void c_abortedByAssumption() {
assumeTrue(false, "database is down");
EVENTS.add("body c_abortedByAssumption");
}
}
@@ -0,0 +1,27 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
class OsConditionalTest {
@Test
@EnabledOnOs(OS.WINDOWS)
void testWindowsRegistryAccess() {
System.out.println("Executing Windows-specific logic.");
}
@Test
@EnabledOnOs({OS.LINUX, OS.MAC})
void testUnixSocketBehavior() {
System.out.println("Executing Unix-specific logic.");
}
@Test
@DisabledOnOs(OS.SOLARIS)
void runEverywhereButSolaris() {
System.out.println("Solaris is not supported for this module.");
}
}
@@ -0,0 +1,21 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
class PipelineTest {
@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
void performanceBenchmark() {
System.out.println("Benchmark running on dedicated CI worker.");
}
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "STAGING")
void integrationWithExternalAPI() {
System.out.println("Connecting to Staging API...");
}
}
@@ -0,0 +1,40 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
class StackingConditionalTest {
/** Both conditions are false on Linux/macOS; the report names only one of them. */
@Test
@EnabledOnOs(OS.WINDOWS)
@EnabledIfSystemProperty(named = "never.set", matches = ".*")
void bothConditionsFalse() {
}
/** The first is true on Linux, so the second one decides. */
@Test
@EnabledOnOs(OS.LINUX)
@EnabledIfSystemProperty(named = "never.set", matches = ".*")
void onlySecondConditionFalse() {
}
@Test
@EnabledOnOs(value = OS.WINDOWS, disabledReason = "Needs the Windows registry")
void withOwnReason() {
}
@Test
@EnabledIf("alwaysFalse")
void methodConditionDefaultReason() {
}
@Test
@EnabledIf(value = "alwaysFalse", disabledReason = "feature flag is off")
void methodConditionOwnReason() {
}
static boolean alwaysFalse() {
return false;
}
}
@@ -0,0 +1,21 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
class SystemPropertyTest {
@Test
@EnabledIfSystemProperty(named = "test.severity", matches = "critical|high")
void runOnlyHighSeverityTests() {
System.out.println("Running a high-priority test case.");
}
@Test
@EnabledIfSystemProperty(named = "user.language", matches = "en")
void testEnglishLocalization() {
System.out.println("Testing English locale specific logic.");
}
}
@@ -0,0 +1,28 @@
package com.ankurm.tutorials.junit.conditional.fixtures;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.*;
import java.time.DayOfWeek;
import java.time.LocalDate;
public class WeekendCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
DayOfWeek today = LocalDate.now().getDayOfWeek();
if (today == DayOfWeek.SATURDAY || today == DayOfWeek.SUNDAY) {
return ConditionEvaluationResult.enabled("It is the weekend!");
}
return ConditionEvaluationResult.disabled("Skipping: Today is " + today);
}
}
// Usage in your test class
class CustomConditionTest {
@Test
@ExtendWith(WeekendCondition.class)
void weeklyBackupStabilityTest() {
System.out.println("Running heavy weekend-only backup test.");
}
}