skip to content

What does @TestInstance(Lifecycle.PER_CLASS) change about how JUnit 5 runs a test class, and what does it enable?

level: middleimportance: should knowfreq 48%

answer

  1. PER_CLASS = one instance reused for all tests
  2. Unlocks non-static @BeforeAll/@AfterAll
  3. Non-static @MethodSource/@TestFactory allowed
  4. Lose isolation: fields persist between tests
  5. Set globally via junit.jupiter.testinstance.lifecycle.default

basics

~10 s

@TestInstance(Lifecycle.PER_CLASS) tells JUnit to create just one instance of the test class and reuse it for every @Test. Because there's now a shared instance, @BeforeAll and @AfterAll can be non-static methods.

solid answer

~50 s

Annotating the test class with @TestInstance(TestInstance.Lifecycle.PER_CLASS) switches from the default per-method instantiation to a single shared instance reused across every @Test in the class. The main practical payoff is that @BeforeAll and @AfterAll no longer need to be static — since there's one persistent instance, they become ordinary instance methods that can touch instance fields. This is handy when the once-per-class setup naturally wants instance state (e.g. a field-injected dependency, or @MethodSource factory methods that JUnit also allows to be non-static under PER_CLASS). The cost is that instance fields now survive between tests, so you lose automatic isolation: tests can leak state into each other and may become order-dependent unless you reset shared mutable state in @BeforeEach. You enable it per-class via the annotation, or globally via the junit.jupiter.testinstance.lifecycle.default configuration parameter. Use it deliberately, not as a default.

code

java · 16 lines
java
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ReportTest {
    private List<String> rows; // shared instance state

    @BeforeAll               // non-static thanks to PER_CLASS
    void buildOnce() {
        rows = new ArrayList<>(loadExpensiveRows());
    }

    @BeforeEach
    void resetView() {
        // reset mutable per-test state so tests stay independent
    }

    @Test void countsRows() { assertFalse(rows.isEmpty()); }
}

go deeper

for a junior

Knows PER_CLASS reuses one instance and lets @BeforeAll be non-static.

for a middle

Explains the enabled features (non-static lifecycle/source methods), how to enable it per-class and globally, and that isolation is lost.

for a senior

Weighs when sharing is safe (immutable fixtures) vs dangerous (mutable state), and prescribes @BeforeEach resets to keep determinism.

for a principal

Sets suite-wide policy on lifecycle defaults, balancing fixture cost and parallelism against the determinism/maintainability risk of shared state.

## Recap: the default JUnit 5's default is `Lifecycle.PER_METHOD` — a new test-class instance per `@Test`, giving isolation but forcing `@BeforeAll`/`@AfterAll` to be `static`. (See the companion question on PER_METHOD.) ## What PER_CLASS does Putting `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` on the class flips this: **JUnit constructs the test class exactly once and reuses that single instance for all `@Test` methods in the class.** Every test now runs on the *same object*, so its instance fields persist for the whole class run. ```java @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTest { private DataLoader loader; // shared across all tests now @BeforeAll void loadOnce() { // NON-static is now legal loader = new DataLoader(); loader.load(); } @Test void a() { /* uses loader */ } @Test void b() { /* same loader instance */ } } ``` ## What it enables 1. **Non-static `@BeforeAll`/`@AfterAll`.** Because a single instance exists for the class's whole lifetime, the once-per-class hooks can be ordinary instance methods that read and write instance fields. Under PER_METHOD they had to be static (no shared instance to call them on). 2. **Non-static factory methods for parameterized/dynamic tests.** `@MethodSource`, `@TestFactory` and similar source methods, which normally must be static, may be instance methods under PER_CLASS. This lets a source method use injected instance state. 3. **Naturally sharing expensive setup as instance state** — e.g. a started embedded server or a loaded dataset held in a field, initialized once in `@BeforeAll`. ## How you turn it on - **Per class:** the `@TestInstance(Lifecycle.PER_CLASS)` annotation (it is `@Inherited`, so it applies to subclasses too). - **Globally:** set the configuration parameter `junit.jupiter.testinstance.lifecycle.default = per_class` (e.g. in `junit-platform.properties`), which changes the default for the whole suite. Use this cautiously — it removes isolation everywhere. ## The catch: you give up automatic isolation With one shared instance, **instance-field state survives from test to test.** If `testA()` mutates a shared field and `testB()` assumes its initial value, `testB()` now depends on whether `testA()` ran first. That reintroduces order-dependence and flakiness — exactly what PER_METHOD prevented. The standard remedy is to **reset shared mutable state in `@BeforeEach`** so each test still starts clean, while still benefiting from the once-built immutable parts done in `@BeforeAll`. ## When to choose it - The once-per-class setup genuinely needs instance state, or - A source/factory method needs to be non-static, or - The expensive fixture is **immutable / read-only** after setup (safe to share). Avoid it when tests mutate shared state and you can't cleanly reset it — the isolation you lose usually isn't worth the convenience.

  • Why can @BeforeAll be non-static under PER_CLASS but not under PER_METHOD?
    PER_CLASS keeps a single shared instance for the whole class, so JUnit can invoke @BeforeAll on that instance. PER_METHOD has no single shared instance (one per test), so it must call @BeforeAll statically at the class level.
  • You switched a class to PER_CLASS and now a test only passes when the suite runs in a certain order. What's the likely cause and fix?
    Shared instance-field state is leaking between tests because the instance is reused. Reset the mutable shared state in @BeforeEach (or stop sharing mutable state) so each test starts from a known baseline.

Like keeping one reusable whiteboard for the whole meeting instead of a fresh sheet per topic — efficient, but you must wipe it between topics or old notes confuse the next one.

saying these in an interview costs you the question

  • Saying PER_CLASS improves isolation (it removes the automatic isolation)
  • Believing @BeforeAll must always be static even under PER_CLASS
  • Forgetting that mutable shared state needs resetting in @BeforeEach
  • Thinking PER_CLASS is the framework default

context