W3docs

Java JUnit Test Lifecycle

Test instance lifecycle and per-method vs. per-class behavior in JUnit 5.

Java JUnit Test Lifecycle

Every JUnit 5 test runs inside a well-defined lifecycle: a sequence of setup and teardown hooks that fire around your @Test methods in a guaranteed order. Understanding that order — and the rule that JUnit creates a new instance of the test class for every test method — is what separates flaky, order-dependent test suites from clean, isolated ones. This chapter walks the five lifecycle annotations and the two instance lifecycles JUnit offers.

The five lifecycle annotations

JUnit 5 (the org.junit.jupiter.api package) defines four callback annotations that bracket your tests, plus @Test itself:

AnnotationRunsMethod must be
@BeforeAllOnce, before any test in the classstatic (in the default lifecycle)
@BeforeEachBefore every @Test methodinstance
@TestThe test itselfinstance
@AfterEachAfter every @Test methodinstance
@AfterAllOnce, after all tests have runstatic (in the default lifecycle)

A single test class with three tests therefore fires @BeforeAll once, then @BeforeEach@Test@AfterEach three times, then @AfterAll once.

import org.junit.jupiter.api.*;

class CalculatorTest {
  @BeforeAll  static void initSuite() { System.out.println("once, up front"); }
  @BeforeEach void setUp()           { System.out.println("before each test"); }

  @Test void add()      { Assertions.assertEquals(4, 2 + 2); }
  @Test void subtract() { Assertions.assertEquals(0, 2 - 2); }

  @AfterEach void tearDown()    { System.out.println("after each test"); }
  @AfterAll  static void close() { System.out.println("once, at the end"); }
}

A fresh instance per test method

The most important rule of the lifecycle: by default JUnit constructs a brand-new instance of the test class before each test method. Fields you mutate in one test cannot leak into another, because the next test runs on a different object. This is what makes tests independent of execution order.

class IsolationTest {
  private int counter = 0; // re-initialised for every test

  @Test void first()  { counter++; Assertions.assertEquals(1, counter); }
  @Test void second() { counter++; Assertions.assertEquals(1, counter); } // also 1, not 2
}

Both tests see counter == 1. If JUnit reused one instance, the second test would observe 2 and pass or fail depending on order — exactly the fragility this design prevents.

PER_METHOD vs. PER_CLASS

You can opt out of the per-method instance with @TestInstance(Lifecycle.PER_CLASS). Then JUnit creates one instance for the whole class, instance fields persist across tests, and — as a convenience — @BeforeAll/@AfterAll may be non-static.

AspectPER_METHOD (default)PER_CLASS
Instances createdone per @Testone per class
Instance field statereset each testshared across tests
@BeforeAll/@AfterAllmust be staticmay be instance methods
Best formaximum isolationexpensive shared setup
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.TestInstance.Lifecycle;

@TestInstance(Lifecycle.PER_CLASS)
class SharedFixtureTest {
  @BeforeAll void openConnection() { /* non-static is now legal */ }
  @AfterAll  void closeConnection() { }
}

Reach for PER_CLASS only when setup is genuinely expensive and safe to share. The default gives you isolation for free.

Assertions are how a test reports failure

A lifecycle exists to run assertions. Assertions.assertEquals(expected, actual) throws an AssertionFailedError when the values differ, which aborts that single test (its @AfterEach still runs) and marks it failed — other tests continue.

import static org.junit.jupiter.api.Assertions.*;

@Test void example() {
  assertEquals(42, compute());
  assertTrue(isReady());
  assertThrows(IllegalArgumentException.class, () -> parse("bad"));
}

A worked example: tracing the lifecycle by hand

There is no JUnit runner on this code playground, so the program below models the lifecycle in plain JDK code: it fires the hooks in JUnit's order, contrasts PER_METHOD (a new instance per test) with PER_CLASS (one shared instance), and ends with a tiny self-checking harness in the spirit of assertEquals.

java— editable, runs on the server

What to take from the run:

  • The PER_METHOD block prints instance#1, instance#2, instance#3 for the three tests, proving JUnit's default rule: a new test instance is constructed for every @Test method, so no test can see another test's mutated state.
  • In PER_METHOD every [TEST] line reports counter=1, never 2 or 3. Each instance got its own fresh field, which is why tests stay independent of execution order — the core benefit of the default lifecycle.
  • The PER_CLASS block reuses instance#1 for all three tests, and its counter climbs 1 → 2 → 3. With one shared instance, instance-field state deliberately leaks between tests — useful for expensive shared fixtures, dangerous if you forget it.
  • @BeforeAll and @AfterAll each appear exactly once per block, wrapping the per-test @BeforeEach/@AfterEach pairs that fire three times — the exact nesting order JUnit guarantees around your tests.
  • The closing harness prints PASS: for all three checks; a failed check throws an AssertionError carrying a FAIL: message, mirroring how Assertions.assertEquals aborts a single test with an AssertionFailedError while leaving the others to run.

Practice

Practice

In JUnit 5 with the default test instance lifecycle, how many instances of a test class containing three @Test methods are created when the class runs?