Skip to main content

JUnit @BeforeAll and @AfterAll: One-Time Setup and Teardown

How JUnit @BeforeAll and @AfterAll work on JUnit 6 and 5: why they are static, how @TestInstance(PER_CLASS) lifts that, what happens when setup throws, and how @TempDir and the extension Store clean up shared resources for you. Every claim is backed by a run in a companion repository.

Imagine a test class whose setup starts an embedded database (say it takes four seconds), and every one of its twelve tests pays that price again, because the setup lives in @BeforeEach. Move it to @BeforeAll and the suite gets fast — until the day the database fails to start, the tests are skipped, and the build log shows one confusing error plus a second, unrelated NullPointerException from your cleanup code. This article covers both halves: @BeforeAll and @AfterAll for work that should happen once per test class, why they have to be static (and how @TestInstance(PER_CLASS) lifts that), what happens when they throw, and how to hand shared resources to JUnit so you do not have to clean them up by hand.
Versions. JUnit 6.1.3 (Jupiter, Platform and Launcher), the release Maven Central lists as latest and release for junit-jupiter (last updated 2026-08-07); JUnit 6.0.0 went GA on 2025-09-30 with a Java 17 baseline. JDK 25.0.4.1 running at language level 17, Maven Surefire 3.5.4. Everything below also passes unchanged on JUnit 5.14.4 (-Djunit.version=5.14.4) except one Store method noted in the extension section. Every code block and every output block comes from the lifecycle module of the JUnit_Tutorials repository; the repository has no docs folder on purpose, the deeper material is in the collapsible sections here. This article replaces two earlier posts, one on @BeforeAll and one on @AfterAll, which now point here.

@BeforeAll runs once before the first test, @AfterAll once after the last

JUnit normally gives every test method its own fresh instance of the test class. @BeforeEach and @AfterEach run around each method on that instance, which is what keeps tests independent. @BeforeAll and @AfterAll sit one level up: they run once per test class, around the whole group. Use them for things that are expensive to build and safe to share — a database connection, a started server, a directory of fixture files. (They are the JUnit 5/6 replacements for JUnit 4’s @BeforeClass and @AfterClass.)
@BeforeAlloncetest a()instance #1test b()instance #2@AfterAllonceBy default each test gets a new instance, so the class-level hooks cannot belong to any of them.Green boxes run once per class; blue boxes run once per test method.
The picture shows the two scopes; the instance numbers are the detail that matters in the next section. Here is the smallest class that uses both hooks (Fixtures.java lines 22-34):
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"); }
}
The fixture prints the identity hash of this inside each test. Real output (01-static-default.txt):
@BeforeAll static (no instance yet)
a() instance=342198178
b() instance=1988644427
@AfterAll static
The static hooks appear once each, bookending the two tests, and a() and b() ran on different instances (different numbers). The first line already gives away the rule in the next section: when @BeforeAll ran, there was no instance yet.
@BeforeAll / @AfterAll@BeforeEach / @AfterEach
Runsonce per test classaround every test method
Typical jobstart or stop something expensive and shareable: server, container, connection poolreset or rebuild state so one test cannot influence the next
Must be static?yes, unless @TestInstance(PER_CLASS)no
Cost of overuseshared state leaks between testsslow suites
A useful rule of thumb: use the Each hooks so that one test does not influence the next (isolation); use the All hooks so that the test class does not influence the rest of the build (it leaves nothing running, open or on disk). The per-method companion is covered in JUnit @BeforeEach and @AfterEach: The Complete Test Lifecycle Guide. Going deeper

The hooks are static because no instance exists yet; PER_CLASS changes that

A @BeforeAll method must be static (and void, and not private; public, protected or package-private are fine) because in the default mode it runs before JUnit has created any instance (the first line of the output below shows exactly that), and @AfterAll is not tied to any one of the per-test instances either. The method can only belong to the class itself. The static requirement is a nuisance when the setup wants to fill instance fields. Annotate the class with @TestInstance(TestInstance.Lifecycle.PER_CLASS) and JUnit creates one instance for the whole class, so the hooks may be ordinary instance methods (Fixtures.java lines 39-53):
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); }
}
@BeforeAll instance, counter=100
a() instance=18179709 counter=101
b() instance=18179709 counter=102
@AfterAll instance, counter=102
Same instance number for both tests, and the counter carried over from @BeforeAll through both tests into @AfterAll. (Transcript.)
Trap: a shared instance shares its fields. With PER_CLASS a field changed by one test is still changed in the next; the counter above is that effect on purpose. If your tests rely on instance fields starting clean, reset them in @BeforeEach. The official guide says the same.

The mistake this section exists for: a non-static @BeforeAll without PER_CLASS. On JUnit 6.1.3 (and on 5.14.4, identical output) this is rejected at discovery and nothing in the class runs at all (03-non-static-rejected.txt):
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)
The engine itself reports FAILED with a DiscoveryIssueException, and the test ran line never appears in the log. If you would rather not annotate every class, the configuration parameter junit.jupiter.testinstance.lifecycle.default=per_class changes the default for the whole run; the same fixture then passes (13-default-per-class-config.txt). That is a global behaviour change, so it is an opinion call whether it is worth it: I would annotate the classes that need it, so the reason is visible where the code is. Going deeper

If @BeforeAll throws, no test runs — but @AfterAll still does

This is the failure most people meet first. When a @BeforeAll method throws, JUnit marks the class as failed and skips every test in it; there is no per-test failure to click on. The part that surprises people is what happens next: @AfterAll still runs, even though the setup never finished.
@BeforeAllthrowstest a()not runtest b()not run@AfterAllstill runsresource== nullCleanup that assumes setup succeeded will throw a second, misleading exception.
The diagram is the order of events; the code below is the defensive version (Fixtures.java lines 67-87), where the null check keeps cleanup from making things worse:
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
    }
}
Output (04-beforeall-fails.txt):
@BeforeAll start: about to fail
@AfterAll stop: resource is null
-- results --
FAILED     Fixtures$BeforeAllFails  IllegalStateException: container did not start
Neither a() ran nor b() ran appears, @AfterAll ran and found resource still null, and the only failure reported is the real one: container did not start. Now remove the null check (Fixtures.java lines 89-103):
@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
Fingerprint of unguarded cleanup. The real cause is still the primary failure, but JUnit attaches the cleanup’s NullPointerException to it as a suppressed exception, so the build log now contains a second, unrelated error message. If you ever see a NullPointerException in an @AfterAll together with a setup failure, fix the setup first; the NPE is fallout. Always check that a resource was created before closing it. (The suppressed mechanism is the same one described in Java Exception Handling Deep Dive: Checked vs Unchecked, Suppression, and What Exceptions Actually Cost.)
Going deeper

A failing @AfterAll does not stop the other @AfterAll methods

As a rule, anything you start in @BeforeAll should have a matching cleanup in @AfterAll (or be owned by JUnit, as the last two sections show). Teardown is where one error should not hide the next. JUnit runs every @AfterAll method even if an earlier one throws, and reports the failure against the class afterwards (Fixtures.java lines 107-116):
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"); }
}
a() ran
@AfterAll closeDatabase
@AfterAll deleteFiles
-- results --
SUCCESSFUL a()
FAILED     Fixtures$AfterAllThrows  IllegalStateException: db close failed
Both cleanup methods ran, in the order closeDatabase then deleteFiles, and the class was reported FAILED with db close failed — the test a() itself passed. That is the behaviour you want from independent cleanup steps. Do not read an order into it, though. When a class has several @BeforeAll methods they all run, but in an order that is not the order you wrote them in. The User Guide describes the ordering of test classes and methods as ‘deterministic but intentionally nonobvious’ and I found no promise about hook order, so do not rely on it. In the run below the methods were declared first, second, third (Fixtures.java lines 152-164, output 09-two-beforeall.txt):
second
first
third
a()
If teardown order matters (close the connection before stopping the server), put the steps in one method, in the order you need, with each step in its own try/finally or try-with-resources so one failure does not skip the rest. Going deeper

A shared temp directory: make @TempDir static

Tests that need files should not write into the project directory. A @TempDir field gives each test its own directory by default; if you make the field static, the directory is created once and shared by the whole class, which is exactly what a @BeforeAll that seeds files needs (Fixtures.java lines 195-214):
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)); }
}
Output (11-shared-tempdir.txt):
@BeforeAll seeded junit-<n>
a() reads: seeded once
b() reads: seeded once
@AfterAll dir still exists: true
The seeded file is visible in both tests, the directory still exists when @AfterAll runs, and the test that drives this fixture asserts the directory is gone once the class has finished. You write no cleanup code: JUnit deletes it. (The test replaces the random directory number with <n> so the transcript is stable.)
Why that beats hand-written cleanup. Hand-written directory cleanup in @AfterAll has to cope with a @BeforeAll that died halfway (see the previous sections), which is the run where leftovers are most likely. Letting JUnit own the lifetime removes that question.
Going deeper

For a container or database, let JUnit own it: the extension Store

The pattern so far — start in @BeforeAll, stop in @AfterAll — has two weaknesses. Every class that needs the resource starts its own copy, and every class needs correct cleanup. JUnit’s answer is the extension Store: a key-value store with parent/child scopes, where the root scope lives for the whole test run. If you put an AutoCloseable value in a store, JUnit closes it when that scope ends. A tiny extension does the job (SharedServerExtension.java):
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);
    }
}
root store: one per test runclass scope A@ExtendWith(SharedServerExtension)class scope B@ExtendWith(SharedServerExtension)FakeServer (AutoCloseable)
The diagram shows why this scales: both classes ask the root store for the same key, so the expensive object is created on first request and found on every later one; JUnit closes it when the whole run ends, after the last class. Two fixtures use it (Fixtures.java lines 219-222, Fixtures.java lines 225-228) and neither has an @AfterAll. They run in one launcher execution (12-extension-store.txt):
server STARTED
UsesServerA test
UsesServerB test
server CLOSED
One start, both classes served, one close at the very end.
Modern API note, verified against the jar. The older way was to implement ExtensionContext.Store.CloseableResource; it is deprecated since 5.13 in favour of plain AutoCloseable (checked with javap on 6.1.3). On JUnit 6, Store.computeIfAbsent(Class) is the preferred lookup, and the older getOrComputeIfAbsent(Class) is deprecated since 6.0. computeIfAbsent(Class) is new in 6.0: on 5.14.4 the call fails with NoSuchMethodError, and swapping in getOrComputeIfAbsent(Class) makes the identical suite pass there.
Libraries wrap this same idea: Testcontainers’ JUnit integration and Spring’s test context cache both give you a shared, run-scoped resource without a hand-written @AfterAll (I did not run either for this article, so treat that as background, not a measurement). Going deeper

With inheritance and @Nested classes, know who runs first

Inheritance. Superclass @BeforeAll methods run before subclass ones, and @AfterAll runs in the reverse order, so a base class can set up what subclasses rely on (Fixtures.java lines 128-137, output 07-inheritance.txt):
Base @BeforeAll
Sub @BeforeAll
Sub a()
Sub @AfterAll
Base @AfterAll
A surprise worth knowing. For instance methods, overriding replaces the superclass version. A static @BeforeAll that has the same signature as one in the superclass only hides it — and JUnit still runs both, superclass first (Fixtures.java lines 144-150, output 08-hiding.txt):
HidingBase.init
HidingSub.init
HidingSub a()
If you expected the subclass to replace the base setup, you would be wrong on this run (JUnit 6.1.3 and 5.14.4 produced the same transcript). Treat same-named static hooks as a smell: give them different names so the intent is obvious. Nested classes. A @Nested class is an inner class, and its hooks run around its own tests. Since Java 16 inner classes may declare static members, and on the Java 17 baseline of JUnit 6 a static @BeforeAll in a @Nested class works (Fixtures.java lines 168-189, output 10-nested.txt):
Outer @BeforeAll
outer test
Inner static @BeforeAll
inner test
Inner static @AfterAll
Outer @AfterAll
The outer @BeforeAll ran first, the outer test ran, then the nested class’s hook ran just before inner test and its teardown ran before the outer @AfterAll. Note that the nested hooks run when the nested class starts, not when the outer class does. Going deeper
Reference: what is verified about JUnit 6 versus JUnit 5 for these hooks
  • GA and baseline: JUnit 6.0.0 was released on 2025-09-30 with a Java 17 baseline and one version number for Platform, Jupiter and Vintage (6.1.3 release notes). Maven Central’s junit-jupiter metadata lists 6.1.3 as latest and release.
  • Behaviour of the hooks: identical on 6.1.3 and 5.14.4 for every scenario in this article except Store.computeIfAbsent(Class), which is 6.0 or later (run both with mvn test and mvn test -Djunit.version=5.14.4).
  • Kotlin: JUnit 6 lets test and lifecycle methods use suspend (from the 6.0.0 release notes); not exercised here.
  • Not covered: @BeforeAll on interface default methods (needs PER_CLASS, per the User Guide) and parallel execution of classes; I did not run either.
Related posts: the JUnit 6 hub, JUnit 6 Test Lifecycle Explained, and the per-method companion JUnit @BeforeEach and @AfterEach.
Should you reach for @BeforeAll at all? That is opinion. Prefer @BeforeEach and cheap setup by default, because it keeps tests independent. Move work to @BeforeAll only when it is measurably expensive and read-only (or reset between tests). For anything that owns a process, port or directory, prefer a static @TempDir or a Store-owned AutoCloseable over hand-written @AfterAll code, since JUnit owns their cleanup.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.