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 -
@BeforeAlland@AfterAllmethods must be static - Overusing @BeforeAll - If setup is different for each test, use
@BeforeEach - Shared mutable state - Avoid modifying fields in
@BeforeAllthat tests also modify - No cleanup - Always clean up resources in
@AfterEachor@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
}
}