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:
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
a() ran
|
||||||
|
@AfterAll closeDatabase
|
||||||
|
@AfterAll deleteFiles
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL a()
|
||||||
|
FAILED Fixtures$AfterAllThrows IllegalStateException: db close failed
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Base @BeforeAll
|
||||||
|
Sub @BeforeAll
|
||||||
|
Sub a()
|
||||||
|
Sub @AfterAll
|
||||||
|
Base @AfterAll
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL a()
|
||||||
|
SUCCESSFUL Fixtures$Sub
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
HidingBase.init
|
||||||
|
HidingSub.init
|
||||||
|
HidingSub a()
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL a()
|
||||||
|
SUCCESSFUL Fixtures$HidingSub
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
second
|
||||||
|
first
|
||||||
|
third
|
||||||
|
a()
|
||||||
|
-- results --
|
||||||
|
SUCCESSFUL a()
|
||||||
|
SUCCESSFUL Fixtures$TwoBeforeAllInOneClass
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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/<name> relative to the module directory. */
|
||||||
|
public static void write(String name, List<String> lines) {
|
||||||
|
try {
|
||||||
|
Path dir = Path.of("output");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
Files.write(dir.resolve(name), lines);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user