Labs ICT
โญ Pro Login

Test Lifecycle in Detail

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
    

๐Ÿงช Quick Quiz

What is the purpose of a test lifecycle?