Labs ICT
โญ Pro Login

@BeforeEach, @AfterEach, @BeforeAll, @AfterAll

One of the most common patterns in testing is setting up resources before tests and cleaning them up afterward. JUnit 5 gives you four lifecycle annotations to handle exactly this. Let us see how they work and when to use each one.

The Four Lifecycle Annotations


+---------------+---------------------+----------------------------------+
|  Annotation   |  Scope              |  When it runs                    |
+---------------+---------------------+----------------------------------+
| @BeforeAll    |  Once per class     |  Before ANY test method          |
| @BeforeEach   |  Once per test      |  Before EACH test method         |
| @AfterEach    |  Once per test      |  After EACH test method          |
| @AfterAll     |  Once per class     |  After ALL test methods          |
+---------------+---------------------+----------------------------------+

Execution order for two tests:

@BeforeAll (once)
  โ”œโ”€โ”€ @BeforeEach
  โ”‚     โ””โ”€โ”€ test1
  โ”œโ”€โ”€ @AfterEach
  โ”œโ”€โ”€ @BeforeEach
  โ”‚     โ””โ”€โ”€ test2
  โ””โ”€โ”€ @AfterEach
@AfterAll (once)
    

@BeforeEach and @AfterEach

These run before and after every test method. Perfect for setting up fresh test data or cleaning up after each test:


class UserServiceTest {

    private UserService userService;
    private User testUser;

    @BeforeEach
    void setUp() {
        userService = new UserService();
        testUser = new User("John", "john@example.com");
    }

    @AfterEach
    void tearDown() {
        userService.clearAll();
        testUser = null;
    }

    @Test
    void shouldAddUser() {
        userService.add(testUser);
        assertTrue(userService.exists("John"));
    }

    @Test
    void shouldRemoveUser() {
        userService.add(testUser);
        userService.remove("John");
        assertFalse(userService.exists("John"));
    }
}
    

Notice how each test starts with a fresh userService and testUser. This ensures tests do not interfere with each other โ€” each test is completely independent.

@BeforeAll and @AfterAll

These run once for the entire test class. Use them for expensive setup operations like starting a database, loading a configuration file, or creating a shared resource:


class DatabaseTest {

    private static Connection connection;

    @BeforeAll
    static void setUpDatabase() {
        // This runs ONCE before all tests
        connection = DriverManager.getConnection(
            "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1"
        );
        // Create tables, insert seed data
        connection.createStatement().execute(
            "CREATE TABLE users (id INT, name VARCHAR(50))"
        );
    }

    @AfterAll
    static void closeDatabase() throws SQLException {
        // This runs ONCE after all tests
        connection.close();
    }

    @Test
    void shouldInsertUser() throws SQLException {
        connection.createStatement().execute(
            "INSERT INTO users VALUES (1, 'John')"
        );
        ResultSet rs = connection.createStatement()
            .executeQuery("SELECT * FROM users WHERE id = 1");
        assertTrue(rs.next());
        assertEquals("John", rs.getString("name"));
    }
}
    

Important: @BeforeAll and @AfterAll methods must be static (unless you use @TestInstance(PER_CLASS)).

Real-World Example: Testing a File Service


class FileServiceTest {

    private static final String TEST_DIR = "test-output";
    private FileService fileService;

    @BeforeAll
    static void createTestDirectory() {
        new File(TEST_DIR).mkdirs();
    }

    @AfterAll
    static void deleteTestDirectory() throws IOException {
        Files.walk(Paths.get(TEST_DIR))
            .sorted(Comparator.reverseOrder())
            .map(Path::toFile)
            .forEach(File::delete);
    }

    @BeforeEach
    void setUp() {
        fileService = new FileService(TEST_DIR);
    }

    @AfterEach
    void cleanUpFiles() {
        // Remove any files created during a test
        File dir = new File(TEST_DIR);
        for (File file : dir.listFiles()) {
            if (file.isFile()) file.delete();
        }
    }

    @Test
    void shouldCreateFile() {
        fileService.createFile("test.txt", "hello");
        assertTrue(new File(TEST_DIR, "test.txt").exists());
    }

    @Test
    void shouldWriteContent() throws IOException {
        fileService.createFile("data.txt", "world");
        String content = Files.readString(
            Paths.get(TEST_DIR, "data.txt")
        );
        assertEquals("world", content);
    }
}
    

Common Mistakes

Here are some pitfalls to avoid:

  • Forgetting static - @BeforeAll and @AfterAll methods must be static
  • Overusing @BeforeAll - If setup is different for each test, use @BeforeEach
  • Shared mutable state - Avoid modifying fields in @BeforeAll that tests also modify
  • No cleanup - Always clean up resources in @AfterEach or @AfterAll

// BAD: Shared state leads to flaky tests
class BadTest {
    private List<String> items = new ArrayList<>(); // Shared!

    @Test
    void test1() {
        items.add("a");
        assertEquals(1, items.size()); // Passes
    }

    @Test
    void test2() {
        items.add("b");
        assertEquals(1, items.size()); // FAILS! items has 2 elements
    }
}

// GOOD: Fresh state for each test
class GoodTest {
    private List<String> items;

    @BeforeEach
    void setUp() {
        items = new ArrayList<>(); // Fresh list each time
    }

    @Test
    void test1() {
        items.add("a");
        assertEquals(1, items.size()); // Passes
    }

    @Test
    void test2() {
        items.add("b");
        assertEquals(1, items.size()); // Passes
    }
}
    

๐Ÿงช Quick Quiz

What does @BeforeAll do?