skip to content

Why must @BeforeAll/@AfterAll be static under the default lifecycle, and what subtle bugs arise from mixing static fixture state with per-method test instances?

level: seniorimportance: nice to knowfreq 26%

answer

  1. No single instance per class ⇒ static @BeforeAll
  2. Static fields shared by all tests, never reset
  3. Static lifetime = class loader, can cross classes
  4. Parallel + static mutable = data race
  5. Fix: immutable, @BeforeEach reset, or PER_CLASS instance field

basics

~20 s

Under the default PER_METHOD lifecycle JUnit makes a new instance per test, so there's no single instance to run a once-per-class method on — hence @BeforeAll/@AfterAll must be static (called on the class). The bug risk: static fixture fields are shared across all tests and aren't reset, so mutations leak between tests.

solid answer

~50 s

Under PER_METHOD JUnit constructs a fresh test instance per @Test, so a once-per-class hook has no single instance to belong to; JUnit therefore requires @BeforeAll/@AfterAll to be static and invokes them at the class level (PER_CLASS removes this requirement by keeping one shared instance). The subtle trap is that the natural way to hold once-built state under PER_METHOD is a static field — and static fields are shared by every test instance and live for the whole class (and class-loader) lifetime, with no automatic reset. So if a test mutates that static state, the mutation persists into later tests, silently reintroducing the order-dependence PER_METHOD was supposed to prevent. Static state is also a parallelism hazard (shared across threads → data races) and can outlive a single class via the class loader, causing cross-class bleed in long-running runners. Mitigations: keep static fixtures immutable, reset mutable parts in @BeforeEach, or prefer PER_CLASS with instance fields when shared mutable setup is unavoidable.

code

java · 12 lines
java
class FlakyStaticTest {
    static int calls = 0;            // class-loader-scoped, shared, never reset

    @BeforeAll static void init() {} // must be static under PER_METHOD

    @Test void first()  { calls++; assertEquals(1, calls); }
    @Test void second() { assertEquals(0, calls); } // breaks if first ran earlier

    // Fix options:
    // @BeforeEach void reset() { calls = 0; }          // reset mutable static, OR
    // @TestInstance(Lifecycle.PER_CLASS) + instance field 'calls'
}

go deeper

for a junior

Knows @BeforeAll/@AfterAll must be static by default and that static fields are shared.

for a middle

Explains the no-single-instance reason and that static state isn't reset between tests, causing leakage.

for a senior

Connects static lifetime to the class loader, flags parallelism data races, and prescribes immutable/reset/PER_CLASS mitigations.

for a principal

Sets conventions to avoid hidden global static state in suites, accounting for class-loader scope, parallel runners, and cross-class isolation in CI.

## The 'why static' question Under the **default PER_METHOD** lifecycle, JUnit creates a **new instance of the test class for every `@Test`**. A `@BeforeAll` method is supposed to run **once for the whole class**, before *any* test — so which instance would it run on? None in particular: at the time `@BeforeAll` runs, no test instance has been created for a specific test yet, and there is no single long-lived instance. The only thing that exists once-per-class and can hold a method JUnit can call without an instance is a **static** method. Hence the rule: under PER_METHOD, `@BeforeAll`/`@AfterAll` **must be `static`**, and JUnit invokes them on the `Class` itself. `@TestInstance(Lifecycle.PER_CLASS)` changes the premise — there *is* a single shared instance for the whole class — so JUnit can call `@BeforeAll`/`@AfterAll` on that instance, and they may be **non-static**. ## The subtle bug: static fixture state Because `@BeforeAll` must be static under PER_METHOD, the natural place to stash what it builds is a **static field**: ```java class OrderTest { static List<String> log = new ArrayList<>(); // shared by ALL instances @BeforeAll static void setup() { /* fill log once */ } @Test void a() { log.add("a"); assertEquals(1, log.size()); } @Test void b() { assertEquals(0, log.size()); } // FAILS if a ran first: log already has 'a' } ``` Key facts about static state in tests: - **Shared across every test instance.** PER_METHOD gives fresh *instance* fields, but **static fields are not reset** — they're one variable for the whole class. So static state defeats the isolation that fresh instances provide. - **No lifecycle reset.** Nothing in JUnit clears a static field between tests; you must reset it yourself. - **Lives as long as the class is loaded.** A static field's lifetime is the **class loader's**, which can span the entire test run — potentially leaking between *different* test classes if they share state through a common static (e.g. a static singleton, a `System.setProperty`, a static cache). - **Parallelism hazard.** With parallel execution, multiple test threads touch the same static field concurrently → **data races** and nondeterministic failures. ## Why this is easy to get wrong The `static` requirement *pushes* you toward static fields, and static fields are exactly the kind of shared mutable state that breaks isolation. So the mechanism meant to express 'once per class' nudges you into a footgun if the fixture is mutable. ## Mitigations (in order of preference) 1. **Keep static fixtures immutable / read-only after `@BeforeAll`.** A loaded read-only dataset or a started server you only query is safe to share, even across threads. 2. **Reset mutable shared state in `@BeforeEach`.** If tests must mutate it, restore a baseline before each test so independence holds. 3. **Prefer PER_CLASS + instance fields** when you need shared *mutable* setup: at least the state is scoped to the one class instance (not class-loader-global) and the intent is explicit — but you still reset mutable parts in `@BeforeEach`. 4. **Avoid hidden global statics** (singletons, system properties, static caches) touched by tests; if unavoidable, restore them in teardown. ## Interview-grade summary 'Static is required under PER_METHOD because there's no single instance for a once-per-class hook. The catch is that static fixture fields are class-loader-scoped, shared by every test, never auto-reset, and unsafe under parallelism — so they quietly recreate order-dependence. Keep them immutable, reset mutable ones, or switch to PER_CLASS with instance fields.'

  • If PER_METHOD gives each test a fresh instance, why doesn't that isolate static fields too?
    Fresh instances only reset INSTANCE fields. A static field belongs to the class, not any instance, so all the fresh instances see and share the same single static variable — it is never reset between tests.
  • How long does a static fixture field live, and why does that matter across test classes?
    It lives as long as its class is loaded — typically the class loader's lifetime, which can span the whole test run. State written to a shared static (e.g. a singleton or system property) can therefore bleed from one test class into another, not just between methods.
  • When is sharing static fixture state actually safe?
    When it's immutable / read-only after setup — e.g. a loaded read-only dataset or a started server you only query. With no mutation there's no leakage and no data race, even under parallel execution.

PER_METHOD hands every test its own notebook, but a static field is a shared bulletin board on the wall — fresh notebooks don't help if everyone scribbles on the same board and nobody erases it.

saying these in an interview costs you the question

  • Saying fresh per-test instances isolate static fields (they isolate only instance fields)
  • Treating static fixture state as auto-reset between tests
  • Ignoring that static state is class-loader-scoped and can leak across classes
  • Using mutable static fixtures with parallel execution and expecting determinism

context