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:
| Annotation | Runs | Method must be |
|---|---|---|
@BeforeAll | Once, before any test in the class | static (in the default lifecycle) |
@BeforeEach | Before every @Test method | instance |
@Test | The test itself | instance |
@AfterEach | After every @Test method | instance |
@AfterAll | Once, after all tests have run | static (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.
| Aspect | PER_METHOD (default) | PER_CLASS |
|---|---|---|
| Instances created | one per @Test | one per class |
| Instance field state | reset each test | shared across tests |
@BeforeAll/@AfterAll | must be static | may be instance methods |
| Best for | maximum isolation | expensive 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.
What to take from the run:
- The
PER_METHODblock printsinstance#1,instance#2,instance#3for the three tests, proving JUnit's default rule: a new test instance is constructed for every@Testmethod, so no test can see another test's mutated state. - In
PER_METHODevery[TEST]line reportscounter=1, never2or3. 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_CLASSblock reusesinstance#1for all three tests, and itscounterclimbs1 → 2 → 3. With one shared instance, instance-field state deliberately leaks between tests — useful for expensive shared fixtures, dangerous if you forget it. @BeforeAlland@AfterAlleach appear exactly once per block, wrapping the per-test@BeforeEach/@AfterEachpairs that fire three times — the exact nesting order JUnit guarantees around your tests.- The closing harness prints
PASS:for all three checks; a failedcheckthrows anAssertionErrorcarrying aFAIL:message, mirroring howAssertions.assertEqualsaborts a single test with anAssertionFailedErrorwhile leaving the others to run.
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?