Understanding the test lifecycle is crucial for writing effective tests. JUnit 5 follows a specific order when executing tests, and knowing this order helps you set up and tear down resources correctly. Let us walk through the entire lifecycle from start to finish.
The Complete Lifecycle
Test Class Loaded
โ
โผ
@BeforeAll (once)
โ
โผ
โโโโ Test Instance Created โโโโโโโโโโโโโโโโโโโ
โ โ
โ @BeforeEach โ
โ โ โ
โ โผ โ
โ @Test method โ
โ โ โ
โ โผ โ
โ @AfterEach โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ (repeat for each @Test method)
โ
@BeforeAll... no, @AfterAll (once)
โ
โผ
Test Complete
Lifecycle in Action
import org.junit.jupiter.api.*;
class LifecycleDemoTest {
LifecycleDemoTest() {
System.out.println("1. Constructor");
}
@BeforeAll
static void beforeAll() {
System.out.println("2. @BeforeAll");
}
@BeforeEach
void beforeEach() {
System.out.println("3. @BeforeEach");
}
@Test
void firstTest() {
System.out.println("4. @Test (first)");
}
@Test
void secondTest() {
System.out.println("4. @Test (second)");
}
@AfterEach
void afterEach() {
System.out.println("5. @AfterEach");
}
@AfterAll
static void afterAll() {
System.out.println("6. @AfterAll");
}
}
Output for two tests:
1. Constructor
2. @BeforeAll
3. @BeforeEach
4. @Test (first)
5. @AfterEach
3. @BeforeEach
4. @Test (second)
5. @AfterEach
6. @AfterAll
Lifecycle Modes
By default, JUnit creates a new test class instance for each test method (PER_METHOD). You can change this:
// Default: new instance per test method
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
class DefaultLifecycleTest {
@Test
void test1() { } // Gets its own instance
@Test
void test2() { } // Gets its own instance
}
// Alternative: single instance for all tests
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SharedInstanceTest {
private int counter = 0;
@Test
void test1() {
counter++; // counter = 1
}
@Test
void test2() {
counter++; // counter = 2 (same instance!)
}
}
With PER_CLASS, @BeforeAll and @AfterAll
do not need to be static. But be careful โ shared state between tests can lead
to flaky, order-dependent tests.
Exception Handling in Lifecycle
class ExceptionLifecycleTest {
@BeforeAll
static void setUpFails() {
throw new RuntimeException("Setup failed!");
}
@Test
void neverRuns() {
// This test is skipped because @BeforeAll failed
}
}
// Result:
// Tests run: 0, Errors: 1
// org.junit.platform.launcher.LauncherException:
// BeforeAll method threw exception
Practical Example: Database Lifecycle
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserRepositoryTest {
private Connection connection;
private UserRepository repository;
@BeforeAll
void setUpDatabase() throws SQLException {
connection = DriverManager.getConnection(
"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
);
connection.createStatement().execute(
"CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(50), email VARCHAR(100))"
);
repository = new UserRepository(connection);
}
@BeforeEach
void insertTestData() throws SQLException {
connection.createStatement().execute(
"INSERT INTO users VALUES (1, 'John', 'john@example.com')"
);
}
@AfterEach
void clearTestData() throws SQLException {
connection.createStatement().execute("DELETE FROM users");
}
@AfterAll
void closeDatabase() throws SQLException {
connection.close();
}
@Test
void shouldFindUserById() {
User user = repository.findById(1);
assertEquals("John", user.getName());
}
@Test
void shouldInsertNewUser() {
repository.save(new User(2, "Jane", "jane@example.com"));
assertEquals(2, repository.findAll().size());
}
}
Lifecycle Callbacks
JUnit 5 also provides lifecycle callbacks for more fine-grained control:
// Before test preparation
@BeforeAll / @AfterAll
// Before/after each test
@BeforeEach / @AfterEach
// Around advice (via extensions)
@BeforeAllCallback
AfterAllCallback
BeforeEachCallback
AfterEachCallback
BeforeTestExecutionCallback
AfterTestExecutionCallback