Add lifecycle module: @BeforeAll/@AfterAll demos on JUnit 6.1.3

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
2026-09-30 19:02:06 +00:00
co-authored by Claude Sonnet 5.5
parent 5ed079dff3
commit cfc6c79fc3
22 changed files with 689 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
target/
+50
View File
@@ -0,0 +1,50 @@
# JUnit @BeforeAll / @AfterAll Demonstration
Runnable companion to the ankurm.com article "JUnit @BeforeAll and @AfterAll: One-Time Setup and Teardown".
Every scenario is a small fixture class in `Fixtures.java`. The real tests execute each fixture through the
JUnit Launcher, assert on what happened, and write the event log to `output/`, so each transcript quoted in
the article is also an assertion that fails the build when it stops being true.
## Versions
| Item | Version |
|------|---------|
| JUnit (default, `junit.version`) | 6.1.3 (Jupiter, Platform, Launcher) |
| Also run green on | 5.14.4 (`mvn test -Djunit.version=5.14.4`; see note on `SharedServerExtension`) |
| Java | 17 language level (`maven.compiler.release`), run on JDK 25.0.4.1 |
| Maven Surefire | 3.5.4 |
## Scenarios
| Fixture | What it shows | Output |
|---------|---------------|--------|
| `StaticHooks` | default lifecycle: static hooks, a new instance per test | `01-static-default.txt` |
| `PerClassHooks` | `@TestInstance(PER_CLASS)`: instance hooks, one shared instance | `02-per-class.txt` |
| `NonStaticWithoutPerClass` | the classic mistake; discovery fails | `03-non-static-rejected.txt` |
| `BeforeAllFails` | failing `@BeforeAll` skips tests, `@AfterAll` still runs | `04-beforeall-fails.txt` |
| `BeforeAllFailsUnguarded` | unguarded `@AfterAll` NPE is attached as suppressed | `05-afterall-unguarded.txt` |
| `AfterAllThrows` | one failing `@AfterAll` does not stop the next | `06-afterall-throws.txt` |
| `Sub` / `Base` | inheritance order | `07-inheritance.txt` |
| `HidingSub` / `HidingBase` | a hidden static `@BeforeAll` still runs | `08-hiding.txt` |
| `TwoBeforeAllInOneClass` | order among several hooks in one class | `09-two-beforeall.txt` |
| `OuterWithNested` | static `@BeforeAll` inside `@Nested` | `10-nested.txt` |
| `SharedTempDir` | static `@TempDir` shared by the class | `11-shared-tempdir.txt` |
| `UsesServerA` / `UsesServerB` | root-store `AutoCloseable` owned by an extension | `12-extension-store.txt` |
| `NonStaticWithoutPerClass` + config | `junit.jupiter.testinstance.lifecycle.default=per_class` | `13-default-per-class-config.txt` |
Note: `SharedServerExtension` uses `Store.computeIfAbsent(Class)`, which exists from 6.0. On 5.14.x use
`getOrComputeIfAbsent(Class)` (deprecated since 6.0); with that one-word change the whole suite passes on 5.14.4.
## Prerequisites
- JDK 17 or higher
- Maven 3.6+
## How to Run
```bash
mvn test
```
The transcripts in `output/` are regenerated by that command. Instance hash codes in `01` and `02` change per run.
+8
View File
@@ -0,0 +1,8 @@
@BeforeAll static (no instance yet)
a() instance=342198178
b() instance=1988644427
@AfterAll static
-- results --
SUCCESSFUL a()
SUCCESSFUL b()
SUCCESSFUL Fixtures$StaticHooks
+8
View File
@@ -0,0 +1,8 @@
@BeforeAll instance, counter=100
a() instance=18179709 counter=101
b() instance=18179709 counter=102
@AfterAll instance, counter=102
-- results --
SUCCESSFUL a()
SUCCESSFUL b()
SUCCESSFUL Fixtures$PerClassHooks
@@ -0,0 +1,6 @@
-- results --
FAILED <engine> DiscoveryIssueException: TestEngine with ID 'junit-jupiter' encountered a critical issue during test discovery:
(1) [ERROR] @BeforeAll method 'void com.ankurm.tutorials.junit.lifecycle.Fixtures$NonStaticWithoutPerClass.setupAll()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).
Source: MethodSource [className = 'com.ankurm.tutorials.junit.lifecycle.Fixtures$NonStaticWithoutPerClass', methodName = 'setupAll', methodParameterTypes = '']
at com.ankurm.tutorials.junit.lifecycle.Fixtures$NonStaticWithoutPerClass.setupAll(SourceFile:0)
+4
View File
@@ -0,0 +1,4 @@
@BeforeAll start: about to fail
@AfterAll stop: resource is null
-- results --
FAILED Fixtures$BeforeAllFails IllegalStateException: container did not start
@@ -0,0 +1,4 @@
@AfterAll stop (unguarded)
-- results --
FAILED Fixtures$BeforeAllFailsUnguarded IllegalStateException: container did not start
suppressed NullPointerException: Cannot invoke "java.lang.AutoCloseable.close()" because "com.ankurm.tutorials.junit.lifecycle.Fixtures$BeforeAllFailsUnguarded.resource" is null
+6
View File
@@ -0,0 +1,6 @@
a() ran
@AfterAll closeDatabase
@AfterAll deleteFiles
-- results --
SUCCESSFUL a()
FAILED Fixtures$AfterAllThrows IllegalStateException: db close failed
+8
View File
@@ -0,0 +1,8 @@
Base @BeforeAll
Sub @BeforeAll
Sub a()
Sub @AfterAll
Base @AfterAll
-- results --
SUCCESSFUL a()
SUCCESSFUL Fixtures$Sub
+6
View File
@@ -0,0 +1,6 @@
HidingBase.init
HidingSub.init
HidingSub a()
-- results --
SUCCESSFUL a()
SUCCESSFUL Fixtures$HidingSub
+7
View File
@@ -0,0 +1,7 @@
second
first
third
a()
-- results --
SUCCESSFUL a()
SUCCESSFUL Fixtures$TwoBeforeAllInOneClass
+11
View File
@@ -0,0 +1,11 @@
Outer @BeforeAll
outer test
Inner static @BeforeAll
inner test
Inner static @AfterAll
Outer @AfterAll
-- results --
SUCCESSFUL outerTest()
SUCCESSFUL innerTest()
SUCCESSFUL Inner
SUCCESSFUL Fixtures$OuterWithNested
+8
View File
@@ -0,0 +1,8 @@
@BeforeAll seeded junit-<n>
a() reads: seeded once
b() reads: seeded once
@AfterAll dir still exists: true
-- results --
SUCCESSFUL a()
SUCCESSFUL b()
SUCCESSFUL Fixtures$SharedTempDir
+9
View File
@@ -0,0 +1,9 @@
server STARTED
UsesServerA test
UsesServerB test
server CLOSED
-- results --
SUCCESSFUL a()
SUCCESSFUL Fixtures$UsesServerA
SUCCESSFUL b()
SUCCESSFUL Fixtures$UsesServerB
@@ -0,0 +1,5 @@
setupAll ran
test ran
-- results --
SUCCESSFUL a()
SUCCESSFUL Fixtures$NonStaticWithoutPerClass
+53
View File
@@ -0,0 +1,53 @@
<?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>lifecycle-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 -->
<junit.version>6.1.3</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- used by RunHelper to execute the deliberately failing fixtures programmatically -->
<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>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,99 @@
package com.ankurm.tutorials.junit.lifecycle;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.util.List;
import org.junit.jupiter.api.Test;
/** Failure modes, inheritance, nesting and @TempDir: each test runs a fixture and pins what happened. */
class BehaviourTest {
@Test
void beforeAllFailureSkipsTestsButAfterAllStillRuns() {
List<String> log = RunHelper.run(Fixtures.BeforeAllFails.class);
Events.write("04-beforeall-fails.txt", log);
assertTrue(log.contains("@BeforeAll start: about to fail"));
assertFalse(log.contains("a() ran"));
assertFalse(log.contains("b() ran"));
assertTrue(log.contains("@AfterAll stop: resource is null"));
assertTrue(log.stream().anyMatch(s -> s.startsWith("FAILED") && s.contains("container did not start")));
}
@Test
void unguardedAfterAllAddsASecondFailure() {
List<String> log = RunHelper.run(Fixtures.BeforeAllFailsUnguarded.class);
Events.write("05-afterall-unguarded.txt", log);
assertTrue(log.contains("@AfterAll stop (unguarded)"));
// the AfterAll NullPointerException does not replace the real failure; it is attached to it as suppressed
assertTrue(log.stream().anyMatch(s -> s.contains("IllegalStateException: container did not start")));
assertTrue(log.stream().anyMatch(s -> s.contains("suppressed NullPointerException")));
}
@Test
void failingAfterAllDoesNotStopTheOtherAfterAll() {
List<String> log = RunHelper.run(Fixtures.AfterAllThrows.class);
Events.write("06-afterall-throws.txt", log);
assertTrue(log.contains("@AfterAll closeDatabase"));
assertTrue(log.contains("@AfterAll deleteFiles"));
assertTrue(log.stream().anyMatch(s -> s.startsWith("FAILED") && s.contains("db close failed")));
}
@Test
void superclassBeforeAllFirstAndAfterAllLast() {
List<String> log = RunHelper.run(Fixtures.Sub.class);
Events.write("07-inheritance.txt", log);
assertEquals(List.of("Base @BeforeAll", "Sub @BeforeAll", "Sub a()", "Sub @AfterAll", "Base @AfterAll"),
log.subList(0, 5));
}
@Test
void staticBeforeAllHiddenBySubclassStillRunsInTheSuperclass() {
List<String> log = RunHelper.run(Fixtures.HidingSub.class);
Events.write("08-hiding.txt", log);
// unlike an overridden instance method, a hidden static @BeforeAll is NOT replaced: both run, super first
assertEquals(List.of("HidingBase.init", "HidingSub.init", "HidingSub a()"), log.subList(0, 3));
}
@Test
void multipleBeforeAllMethodsInOneClassRunInAnOrderYouShouldNotRelyOn() {
List<String> log = RunHelper.run(Fixtures.TwoBeforeAllInOneClass.class);
Events.write("09-two-beforeall.txt", log);
// all three run, then the test; the order of the three is deterministic but deliberately non-obvious
assertEquals(3, log.indexOf("a()"));
assertEquals(List.of("second", "first", "third"), log.subList(0, 3)); // observed, not promised
assertEquals(java.util.Set.of("first", "second", "third"), new java.util.HashSet<>(log.subList(0, 3)));
}
@Test
void staticBeforeAllInNestedClassWorksOnJava17() {
List<String> log = RunHelper.run(Fixtures.OuterWithNested.class);
Events.write("10-nested.txt", log);
assertEquals(List.of("Outer @BeforeAll", "Inner static @BeforeAll", "inner test", "Inner static @AfterAll"),
log.stream().filter(s -> !s.equals("outer test")).limit(4).toList());
assertTrue(log.indexOf("Outer @BeforeAll") < log.indexOf("Inner static @BeforeAll"));
assertTrue(log.indexOf("Inner static @AfterAll") < log.indexOf("Outer @AfterAll"));
}
@Test
void staticTempDirIsSharedAndDeletedAfterAfterAll() {
List<String> log = RunHelper.run(Fixtures.SharedTempDir.class);
Events.write("11-shared-tempdir.txt", log);
assertTrue(log.contains("a() reads: seeded once"));
assertTrue(log.contains("b() reads: seeded once"));
assertTrue(log.contains("@AfterAll dir still exists: true"));
assertFalse(Files.exists(Fixtures.capturedTempDir), "deleted once the class is finished");
}
@Test
void rootStoreResourceIsStartedOnceAndClosedByJUnit() {
List<String> log = RunHelper.run(Fixtures.UsesServerA.class, Fixtures.UsesServerB.class);
Events.write("12-extension-store.txt", log);
assertEquals(1, log.stream().filter(s -> s.equals("server STARTED")).count());
assertEquals(1, log.stream().filter(s -> s.equals("server CLOSED")).count());
assertTrue(log.indexOf("server STARTED") < log.indexOf("UsesServerA test"));
assertTrue(log.indexOf("server CLOSED") > log.indexOf("UsesServerB test"));
}
}
@@ -0,0 +1,31 @@
package com.ankurm.tutorials.junit.lifecycle;
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/&lt;name&gt; 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,229 @@
package com.ankurm.tutorials.junit.lifecycle;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.IOException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.io.TempDir;
/**
* Fixture classes. They are not named *Test, so Surefire never runs them on its own;
* the real tests execute them through the Launcher (see RunHelper) and assert on the outcome.
*/
final class Fixtures {
private Fixtures() {}
// ---- 1. the default lifecycle: static hooks, one instance per test method --------------
static class StaticHooks {
@BeforeAll
static void setupAll() { Events.add("@BeforeAll static (no instance yet)"); }
@Test
void a() { Events.add("a() instance=" + System.identityHashCode(this)); }
@Test
void b() { Events.add("b() instance=" + System.identityHashCode(this)); }
@AfterAll
static void teardownAll() { Events.add("@AfterAll static"); }
}
// ---- 2. PER_CLASS: instance hooks, one shared instance -----------------------------
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
static class PerClassHooks {
private int sharedCounter;
@BeforeAll
void setupAll() { sharedCounter = 100; Events.add("@BeforeAll instance, counter=" + sharedCounter); }
@Test
void a() { sharedCounter++; Events.add("a() instance=" + System.identityHashCode(this) + " counter=" + sharedCounter); }
@Test
void b() { sharedCounter++; Events.add("b() instance=" + System.identityHashCode(this) + " counter=" + sharedCounter); }
@AfterAll
void teardownAll() { Events.add("@AfterAll instance, counter=" + sharedCounter); }
}
// ---- 3. the mistake: non-static @BeforeAll without PER_CLASS ------------------------
static class NonStaticWithoutPerClass {
@BeforeAll
void setupAll() { Events.add("setupAll ran"); }
@Test
void a() { Events.add("test ran"); }
}
// ---- 4. @BeforeAll throws: tests are skipped, @AfterAll still runs ------------------
static class BeforeAllFails {
private static AutoCloseable resource;
@BeforeAll
static void start() throws Exception {
Events.add("@BeforeAll start: about to fail");
throw new IllegalStateException("container did not start");
}
@Test
void a() { Events.add("a() ran"); }
@Test
void b() { Events.add("b() ran"); }
@AfterAll
static void stop() throws Exception {
Events.add("@AfterAll stop: resource is " + (resource == null ? "null" : "set"));
if (resource != null) resource.close(); // the null check is what keeps this from masking the real failure
}
}
static class BeforeAllFailsUnguarded {
private static AutoCloseable resource;
@BeforeAll
static void start() { throw new IllegalStateException("container did not start"); }
@Test
void a() { Events.add("a() ran"); }
@AfterAll
static void stop() throws Exception {
Events.add("@AfterAll stop (unguarded)");
resource.close();
}
}
// ---- 5. two @AfterAll methods, the first one throws ---------------------------------
static class AfterAllThrows {
@Test
void a() { Events.add("a() ran"); }
@AfterAll
static void closeDatabase() { Events.add("@AfterAll closeDatabase"); throw new IllegalStateException("db close failed"); }
@AfterAll
static void deleteFiles() { Events.add("@AfterAll deleteFiles"); }
}
// ---- 6. inheritance ------------------------------------------------------------------
static class Base {
@BeforeAll
static void baseBefore() { Events.add("Base @BeforeAll"); }
@AfterAll
static void baseAfter() { Events.add("Base @AfterAll"); }
}
static class Sub extends Base {
@BeforeAll
static void subBefore() { Events.add("Sub @BeforeAll"); }
@Test
void a() { Events.add("Sub a()"); }
@AfterAll
static void subAfter() { Events.add("Sub @AfterAll"); }
}
static class HidingBase {
@BeforeAll
static void init() { Events.add("HidingBase.init"); }
}
static class HidingSub extends HidingBase {
@BeforeAll
static void init() { Events.add("HidingSub.init"); }
@Test
void a() { Events.add("HidingSub a()"); }
}
static class TwoBeforeAllInOneClass {
@BeforeAll
static void first() { Events.add("first"); }
@BeforeAll
static void second() { Events.add("second"); }
@BeforeAll
static void third() { Events.add("third"); }
@Test
void a() { Events.add("a()"); }
}
// ---- 7. nested classes ---------------------------------------------------------------
static class OuterWithNested {
@BeforeAll
static void outerBefore() { Events.add("Outer @BeforeAll"); }
@AfterAll
static void outerAfter() { Events.add("Outer @AfterAll"); }
@Test
void outerTest() { Events.add("outer test"); }
@Nested
class Inner {
@BeforeAll
static void innerBefore() { Events.add("Inner static @BeforeAll"); }
@AfterAll
static void innerAfter() { Events.add("Inner static @AfterAll"); }
@Test
void innerTest() { Events.add("inner test"); }
}
}
// ---- 8. shared temp directory --------------------------------------------------------
static Path capturedTempDir;
static class SharedTempDir {
@TempDir
static Path shared;
@BeforeAll
static void seed() throws IOException {
capturedTempDir = shared;
Files.writeString(shared.resolve("seed.txt"), "seeded once");
Events.add("@BeforeAll seeded " + shared.getFileName().toString().replaceAll("\\d{6,}", "<n>"));
}
@Test
void a() throws IOException { Events.add("a() reads: " + Files.readString(shared.resolve("seed.txt"))); }
@Test
void b() throws IOException { Events.add("b() reads: " + Files.readString(shared.resolve("seed.txt"))); }
@AfterAll
static void check() { Events.add("@AfterAll dir still exists: " + Files.exists(shared)); }
}
// ---- 9. shared resource owned by an extension, closed by JUnit -------------------------
@org.junit.jupiter.api.extension.ExtendWith(SharedServerExtension.class)
static class UsesServerA {
@Test
void a() { Events.add("UsesServerA test"); }
}
@org.junit.jupiter.api.extension.ExtendWith(SharedServerExtension.class)
static class UsesServerB {
@Test
void b() { Events.add("UsesServerB test"); }
}
}
@@ -0,0 +1,56 @@
package com.ankurm.tutorials.junit.lifecycle;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import java.util.ArrayList;
import java.util.List;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
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;
/** Runs fixture classes through the real Launcher and returns the events plus per-node results. */
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();
}
static List<String> run(Class<?>... classes) {
return run(java.util.Map.of(), classes);
}
static List<String> run(java.util.Map<String, String> config, Class<?>... classes) {
Events.clear();
var b = LauncherDiscoveryRequestBuilder.request();
for (Class<?> c : classes) b.selectors(selectClass(c));
config.forEach(b::configurationParameter);
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());
}
@Override
public void executionFinished(TestIdentifier id, TestExecutionResult r) {
boolean root = id.getParentId().isEmpty();
if (root && r.getStatus() == TestExecutionResult.Status.SUCCESSFUL) return; // engine root, nothing to say
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;
}
}
@@ -0,0 +1,27 @@
package com.ankurm.tutorials.junit.lifecycle;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
/**
* Starts an expensive shared resource once per test run, however many classes use it, and lets
* JUnit close it. Because the value lives in the ROOT store and implements AutoCloseable,
* no @AfterAll is needed anywhere.
*/
public class SharedServerExtension implements BeforeAllCallback {
private static final Namespace NS = Namespace.create(SharedServerExtension.class);
/** Stand-in for a container, embedded database or mock HTTP server. */
public static class FakeServer implements AutoCloseable {
FakeServer() { Events.add("server STARTED"); }
@Override
public void close() { Events.add("server CLOSED"); }
}
@Override
public void beforeAll(ExtensionContext context) {
context.getRoot().getStore(NS).computeIfAbsent(FakeServer.class);
}
}
@@ -0,0 +1,53 @@
package com.ankurm.tutorials.junit.lifecycle;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
/** Default lifecycle: static @BeforeAll/@AfterAll, one new instance per test method. */
class StaticLifecycleTest {
@Test
void staticHooksRunOnceAndInstancesDiffer() {
List<String> log = RunHelper.run(Fixtures.StaticHooks.class);
Events.write("01-static-default.txt", log);
assertEquals(1, log.stream().filter(s -> s.startsWith("@BeforeAll")).count());
assertEquals(1, log.stream().filter(s -> s.startsWith("@AfterAll")).count());
assertEquals("@BeforeAll static (no instance yet)", log.get(0));
String a = log.stream().filter(s -> s.startsWith("a() instance=")).findFirst().orElseThrow();
String b = log.stream().filter(s -> s.startsWith("b() instance=")).findFirst().orElseThrow();
assertNotEquals(a.substring(a.indexOf('=')), b.substring(b.indexOf('=')));
}
@Test
void perClassAllowsInstanceHooksAndSharesOneInstance() {
List<String> log = RunHelper.run(Fixtures.PerClassHooks.class);
Events.write("02-per-class.txt", log);
String a = log.stream().filter(s -> s.startsWith("a() instance=")).findFirst().orElseThrow();
String b = log.stream().filter(s -> s.startsWith("b() instance=")).findFirst().orElseThrow();
assertEquals(a.substring(a.indexOf('='), a.indexOf(" counter")), b.substring(b.indexOf('='), b.indexOf(" counter")));
assertEquals(1, log.stream().filter(s -> s.startsWith("@BeforeAll")).count());
assertTrue(log.contains("a() instance=" + a.substring(a.indexOf('=') + 1, a.indexOf(" counter")) + " counter=101"));
assertTrue(log.contains("@AfterAll instance, counter=102"));
}
@Test
void nonStaticBeforeAllWithoutPerClassIsRejected() {
List<String> log = RunHelper.run(Fixtures.NonStaticWithoutPerClass.class);
Events.write("03-non-static-rejected.txt", log);
assertEquals(0, log.stream().filter(s -> s.startsWith("test ran")).count());
assertTrue(log.stream().anyMatch(s -> s.contains("must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS)")));
}
@Test
void globalDefaultPerClassMakesNonStaticHooksLegal() {
List<String> log = RunHelper.run(Map.of("junit.jupiter.testinstance.lifecycle.default", "per_class"),
Fixtures.NonStaticWithoutPerClass.class);
Events.write("13-default-per-class-config.txt", log);
assertEquals(List.of("setupAll ran", "test ran"), log.subList(0, 2));
}
}