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
| Annotation | Runs | Must be static? | JUnit 4 equivalent |
|---|---|---|---|
@BeforeAll | Once before ALL tests in the class | Yes (default) / No (PER_CLASS) | @BeforeClass |
@BeforeEach | Before EACH individual test method | No | @Before |
@AfterEach | After EACH individual test method | No | @After |
@AfterAll | Once after ALL tests in the class | Yes (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 counters | Loading a large static test dataset from a file |
| Preparing test data that must not be shared | Anything that can be safely shared read-only across all tests |
| Setting up HTTP client with clean state | Starting 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
- JUnit 6 Tutorial: Complete Series Index
- JUnit 6 Project Structure and Best Practices
- Structuring Tests with JUnit 6 Nested Tests
- JUnit 6 Extensions Model: Build Custom Extensions Step-by-Step
- JUnit 6 with Spring Boot: Unit, Slice, and Integration Testing
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.