skip to content

Why does JUnit 5 normally require @BeforeAll and @AfterAll methods to be declared static, and what happens if you leave the keyword off?

level: middleimportance: must knowfreq 52%

answer

  1. new instance per test method
  2. no single instance owns a class-level hook
  3. static binds it to the class
  4. runtime JUnitException, whole class red
  5. @TestInstance(PER_CLASS) lifts the rule

basics

~20 s

By default JUnit creates a new test-class instance for every test method, so there is no single instance a class-level hook could belong to; it must be static to be callable once per class. Without static you get a runtime JUnitException telling you the method must be static, not a compile error.

solid answer

~50 s

With Jupiter's default per-method test-instance behaviour, a **new instance of the test class is constructed for every test method**. A hook that must run exactly once for the class therefore cannot be an instance method — there is no one instance it belongs to, and picking one would be arbitrary. Making it `static` binds it to the class, which is the scope that actually matches. If you omit `static`, it compiles fine — this is a runtime rule enforced by Jupiter, not by javac. At execution the class fails with a `JUnitException` along the lines of *"@BeforeAll method 'x' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS)"*, and every test in the class is reported as failing. Consequences follow from `static`: such hooks can only touch static fields, so shared fixtures held for `@AfterAll` must be static too. Annotating the class with `@TestInstance(Lifecycle.PER_CLASS)` lifts the requirement, since one instance then serves the whole class.

code

java · 23 lines
java
import org.junit.jupiter.api.*;

class ServerTest {

    static EmbeddedServer server;   // must be static: the hooks are static

    @BeforeAll
    static void start() { server = EmbeddedServer.start(); }

    @AfterAll
    static void stop() { server.stop(); }

    @Test void respondsToPing() { /* ... */ }
}

class BrokenTest {
    // Compiles, but at runtime every test in this class fails with a
    // JUnitException: "@BeforeAll method 'setUp' must be static ..."
    @BeforeAll
    void setUp() { }

    @Test void t() { }
}

go deeper

for a junior

Know that @BeforeAll and @AfterAll must be static by default and recognise the error message when they are not.

for a middle

Derive the rule from the per-test instance creation, note it is a runtime JUnitException that fails the whole class, and that the fixture field must be static too.

for a senior

Add the consequences: static shared fixtures need to be immutable or thread-safe, must be released in @AfterAll, and interact with parallel execution; name PER_CLASS as the escape hatch and its cost.

for a principal

Discuss it as a design choice by the framework — instance-per-test buys order independence, and the static requirement is the visible consequence; team conventions should decide when PER_CLASS is worth its state-leak risk.

## The rule Under JUnit Jupiter's default behaviour, methods annotated `@BeforeAll` and `@AfterAll` must be `static`. They must also be non-private and return `void`, like all lifecycle methods. ## Why the rule exists Jupiter constructs a **fresh instance of the test class for every test method**. This is deliberate: it guarantees that instance fields cannot carry state from one test to the next, which is what makes tests order-independent by construction. Now ask what an instance-level `@BeforeAll` would mean. It has to run once per class, but at that point either no instance exists yet (it runs before the first test, hence before the first instance is constructed) or many instances will exist over the class's lifetime. Which one owns the hook? There is no defensible answer, so the framework refuses the configuration. `static` resolves it: a static method is bound to the class, and the class is exactly the scope of a once-per-class hook. `@AfterAll` is symmetric — by the time it runs, every instance created during the class is finished and eligible for collection. ## What you actually see when you forget This is not a compile error; `static` is not part of any Java-level contract here. javac is happy, and the failure surfaces at execution time as a `JUnitException` from Jupiter's validation phase, with a message naming the offending method and stating that it must be static unless the class uses the per-class test-instance setting. Jupiter reports it as a container-level failure, so **all** tests in the class fail — the symptom is "my whole class is red" rather than one broken test. Recognising that message instantly is a practical skill; it is one of the most common first errors when moving from JUnit 4 or from a Kotlin/Groovy background where `static` is expressed differently. ## Consequences of being static Because the hooks are static, they can only read and write **static** state. So the fixture a `@BeforeAll` creates and an `@AfterAll` releases must live in a static field: ```java static EmbeddedServer server; @BeforeAll static void start() { server = EmbeddedServer.start(); } @AfterAll static void stop() { server.stop(); } ``` That static field is shared by every test in the class, which is precisely why the guidance is to keep such fixtures effectively immutable after setup. It is also why static fixtures interact badly with parallel execution unless they are thread-safe or read-only, and why forgetting to null them out or close them in `@AfterAll` can leak across the whole JVM test run. ## The documented exception Annotating the test class `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` makes Jupiter create a single instance for all tests in the class. With one instance existing for the whole class, an instance-level class hook is well-defined, so the static requirement is lifted and `@BeforeAll`/`@AfterAll` may be non-static — and can then use instance fields. That is a genuinely useful combination for classes that need a non-static shared fixture, but the tradeoff is that instance state now persists across tests, so you take on responsibility for resetting anything mutable in `@BeforeEach`. ## Language-specific wrinkles In Kotlin there is no `static`, so the hook goes in a companion object annotated `@JvmStatic`, or the class uses per-class lifecycle instead — a very frequent stumbling block. In Java, `static` on a nested class's hooks has additional constraints because a non-static nested class cannot declare static members at all in older language levels; this is why inner test classes historically needed per-class lifecycle to declare class-level hooks. ## How to answer Don't stop at "because JUnit says so". The chain is: new instance per test → no single instance owns a once-per-class hook → bind it to the class with `static` → therefore its state must be static too → and the per-class instance setting is the documented escape hatch. Adding that the violation is a runtime `JUnitException` failing the whole class, not a compile error, shows you have hit it for real.

  • How do you legitimately write a non-static @BeforeAll, and what do you take on when you do?
    Annotate the test class with @TestInstance(TestInstance.Lifecycle.PER_CLASS). Jupiter then creates one instance for the whole class, so a class-level hook has a well-defined receiver and may be an instance method using instance fields. The cost is that instance state now survives from test to test, so anything mutable must be reset in @BeforeEach or the tests become order-dependent.
  • Is a missing static keyword on @BeforeAll caught at compile time?
    No. It is a JUnit runtime validation, so the code compiles cleanly and the problem only appears when the class executes, as a JUnitException naming the method. Because it is a container-level failure, every test in the class is reported as failing, which can make the cause look bigger than it is until you read the message.

saying these in an interview costs you the question

  • Claiming the compiler rejects a non-static @BeforeAll
  • Saying the rule exists 'for performance' rather than because a new instance is created per test
  • Not knowing @TestInstance(PER_CLASS) lifts the requirement
  • Trying to assign an instance field from a static @BeforeAll
  • In Kotlin, expecting a plain companion-object function to work without @JvmStatic

context