Skip to main content

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

A complete guide to the JUnit 6 test lifecycle: @BeforeAll, @BeforeEach, @AfterEach, @AfterAll. Includes execution order diagrams, PER_METHOD vs PER_CLASS lifecycle comparison, nested class behaviour, and real console output examples.

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.

No Comments yet!

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