JUnit 6 Test Lifecycle Explained (BeforeEach, AfterAll, and More)

The JUnit 6 test lifecycle defines exactly when each setup and teardown method runs relative to your tests. Misunderstanding the lifecycle is one of the most common sources of subtle, order-dependent test failures. This guide covers every lifecycle annotation in depth — @BeforeAll, @BeforeEach, @AfterEach, @AfterAll — with complete code examples, execution order diagrams, and the important differences between the PER_METHOD and PER_CLASS instance lifecycles.

The Four Lifecycle Annotations

AnnotationRunsMust be static?JUnit 4 equivalent
@BeforeAllOnce before ALL tests in the classYes (default) / No (PER_CLASS)@BeforeClass
@BeforeEachBefore EACH individual test methodNo@Before
@AfterEachAfter EACH individual test methodNo@After
@AfterAllOnce after ALL tests in the classYes (default) / No (PER_CLASS)@AfterClass

Complete Lifecycle Example

This example demonstrates all four annotations together. Read the comments and then check the output to see exactly what order they run in:

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

@DisplayName("Order Processing Lifecycle Demo")
class OrderProcessorTest {

    // Simulates a shared resource (e.g. DB connection, test server)
    private static int sharedResourceInitCount = 0;

    // Instance field: reset before every test because JUnit creates
    // a new instance of this class for each test method (PER_METHOD, default)
    private OrderProcessor orderProcessor;

    // ----------------------------------------------------------------
    // @BeforeAll: runs ONCE before any test in this class.
    // Must be static in the default PER_METHOD lifecycle.
    // Use for expensive one-time setup: starting servers, loading fixtures.
    // ----------------------------------------------------------------
    @BeforeAll
    static void initialiseSharedResources() {
        sharedResourceInitCount++;
        System.out.println("[BeforeAll] Shared resources initialised. Count: " + sharedResourceInitCount);
    }

    // ----------------------------------------------------------------
    // @BeforeEach: runs before EVERY individual @Test method.
    // Use for creating a fresh object under test to avoid state pollution.
    // ----------------------------------------------------------------
    @BeforeEach
    void createFreshOrderProcessor() {
        orderProcessor = new OrderProcessor();
        System.out.println("[BeforeEach] Fresh OrderProcessor created");
    }

    @Test
    @DisplayName("Creating an order sets status to PENDING")
    void creatingOrderSetsPendingStatus() {
        Order order = orderProcessor.createOrder("[email protected]", 49.99);
        System.out.println("[Test] creatingOrderSetsPendingStatus running");
        assertEquals(OrderStatus.PENDING, order.getStatus());
    }

    @Test
    @DisplayName("Completing an order sets status to COMPLETED")
    void completingOrderSetsCompletedStatus() {
        Order order = orderProcessor.createOrder("[email protected]", 49.99);
        orderProcessor.completeOrder(order);
        System.out.println("[Test] completingOrderSetsCompletedStatus running");
        assertEquals(OrderStatus.COMPLETED, order.getStatus());
    }

    // ----------------------------------------------------------------
    // @AfterEach: runs after EVERY individual @Test method.
    // Use for cleanup: closing files, resetting mocks, clearing caches.
    // ----------------------------------------------------------------
    @AfterEach
    void cleanUpAfterTest() {
        orderProcessor = null; // help GC, signal we are done with this instance
        System.out.println("[AfterEach] Cleaned up");
    }

    // ----------------------------------------------------------------
    // @AfterAll: runs ONCE after ALL tests in this class have finished.
    // Must be static in the default PER_METHOD lifecycle.
    // Use for releasing shared resources: closing DB connections, stopping servers.
    // ----------------------------------------------------------------
    @AfterAll
    static void releaseSharedResources() {
        System.out.println("[AfterAll] Shared resources released");
    }
}

Console Output

[BeforeAll] Shared resources initialised. Count: 1

[BeforeEach] Fresh OrderProcessor created
[Test] creatingOrderSetsPendingStatus running
[AfterEach] Cleaned up

[BeforeEach] Fresh OrderProcessor created
[Test] completingOrderSetsCompletedStatus running
[AfterEach] Cleaned up

[AfterAll] Shared resources released

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

Notice that @BeforeAll runs exactly once, @BeforeEach and @AfterEach run twice (once per test), and @AfterAll runs exactly once at the end.

Test Instance Lifecycle: PER_METHOD vs PER_CLASS

JUnit 6 creates a new instance of your test class for each test method by default. This is the PER_METHOD lifecycle — it ensures tests do not share instance state. However, you can opt into PER_CLASS to share one instance across all methods:

import org.junit.jupiter.api.*;

// PER_CLASS: JUnit creates exactly ONE instance of this class
// and uses it for ALL test methods.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisplayName("Integration test with shared database connection")
class DatabaseIntegrationTest {

    // NOT static — allowed because PER_CLASS shares one instance
    private Connection databaseConnection;
    private int queryCount = 0; // shared counter across all tests

    @BeforeAll
        // NOT static — allowed with PER_CLASS
    void openConnection() throws Exception {
        databaseConnection = DriverManager.getConnection("jdbc:h2:mem:testdb");
        System.out.println("[BeforeAll] DB connection opened (once)");
    }

    @BeforeEach
    void incrementQueryCounter() {
        queryCount++;
        System.out.println("[BeforeEach] Query #" + queryCount + " about to run");
    }

    @Test
    void insertingUserSucceeds() throws Exception {
        // Uses shared databaseConnection
        assertNotNull(databaseConnection);
    }

    @Test
    void queryingUsersReturnsResults() throws Exception {
        assertNotNull(databaseConnection);
    }

    @AfterAll
        // NOT static — allowed with PER_CLASS
    void closeConnection() throws Exception {
        databaseConnection.close();
        System.out.println("[AfterAll] DB connection closed (once). Total queries: " + queryCount);
    }
}

PER_CLASS Output

[BeforeAll] DB connection opened (once)
[BeforeEach] Query #1 about to run
[BeforeEach] Query #2 about to run
[AfterAll] DB connection closed (once). Total queries: 2

Lifecycle in Nested Test Classes

Lifecycle methods in a @Nested class compose with those of its enclosing class. The outer class’s @BeforeEach runs before the inner class’s @BeforeEach:

@DisplayName("ShoppingCart lifecycle with nested classes")
class ShoppingCartTest {

    private ShoppingCart cart;

    @BeforeEach
    void createEmptyCart() {
        cart = new ShoppingCart();
        System.out.println("[Outer BeforeEach] Empty cart created");
    }

    @Nested
    @DisplayName("When cart has items")
    class WhenCartHasItems {

        @BeforeEach
        void addItemsToCart() {
            // Outer @BeforeEach already ran — cart is not null here
            cart.addItem(new Item("Book", 12.99));
            System.out.println("[Inner BeforeEach] Item added to cart");
        }

        @Test
        @DisplayName("Cart total should reflect added items")
        void cartTotalReflectsItems() {
            assertEquals(12.99, cart.getTotal(), 0.001);
        }

        @AfterEach
        void logCartContents() {
            System.out.println("[Inner AfterEach] Cart has " + cart.getItemCount() + " items");
        }
    }

    @AfterEach
    void clearCart() {
        cart = null;
        System.out.println("[Outer AfterEach] Cart cleared");
    }
}

Nested Lifecycle Execution Order

[Outer BeforeEach] Empty cart created
[Inner BeforeEach] Item added to cart
[Test] Cart total should reflect added items
[Inner AfterEach] Cart has 1 items
[Outer AfterEach] Cart cleared

@BeforeEach vs @BeforeAll: When to Use Which

Use @BeforeEach when…Use @BeforeAll when…
Creating the object under test (fresh instance every time)Opening a database connection or test server (expensive)
Resetting mocks or countersLoading a large static test dataset from a file
Preparing test data that must not be sharedAnything that can be safely shared read-only across all tests
Setting up HTTP client with clean stateStarting an embedded Kafka or Redis server

Frequently Asked Questions (FAQs)

Q1: Why must @BeforeAll and @AfterAll be static by default?

In the default PER_METHOD lifecycle, JUnit creates a new test class instance for each test method. Since each instance is independent, there is no single instance to call a non-static method on before all tests run. Making these methods static is JUnit’s solution: the class itself (not an instance) is used as the receiver. With @TestInstance(PER_CLASS), a single instance is shared, so non-static @BeforeAll/@AfterAll methods become legal.

Q2: Does @BeforeEach in a parent class run before @BeforeEach in a child class?

Yes. When a test class inherits from a base class, the parent’s @BeforeEach runs first, then the child’s. For @AfterEach, the order is reversed: the child’s runs first, then the parent’s. This composes naturally for base class patterns. See JUnit 6 Project Structure and Best Practices for the base class pattern.

Q3: Can I have multiple @BeforeEach methods in one class?

Yes. You can declare multiple @BeforeEach methods in the same class. JUnit 6 will call all of them before each test. However, the order between multiple @BeforeEach methods in the same class is not guaranteed unless you use @TestMethodOrder. For clarity, prefer a single @BeforeEach that calls helper methods.

Q4: What happens if a @BeforeEach method throws an exception?

If a @BeforeEach method throws an exception, the corresponding test method is marked as failed (not skipped), and the @AfterEach method still runs to ensure cleanup happens. The exception’s stack trace is reported alongside the test failure.

Q5: Is PER_CLASS or PER_METHOD better for Spring Boot tests?

For Spring Boot integration tests, PER_CLASS is often preferred because it allows the Spring ApplicationContext to be set up once per class (in a non-static @BeforeAll). Spring’s own @SpringBootTest manages context caching independently, so either lifecycle works. See JUnit 6 with Spring Boot for detailed guidance.

See Also

Conclusion

The JUnit 6 lifecycle is predictable and composable. Use @BeforeAll/@AfterAll for expensive one-time resources, @BeforeEach/@AfterEach for fresh-per-test state, and PER_CLASS when you need non-static all-hooks or a shared connection. Understanding execution order — especially in nested classes and inheritance hierarchies — eliminates an entire class of flaky test issues.

Next: Writing Your First Clean Test in JUnit 6 (Best Practices) — apply the lifecycle knowledge here to write tests that are clear, fast, and maintainable.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.