If you came through the TestNG course, you know @BeforeMethod, @AfterMethod, @BeforeClass, and @AfterClass. JUnit 5 has direct equivalents with different names and one important constraint you need to understand from day one. Get the lifecycle model right and your tests are clean, independent, and fast to debug. Get it wrong and you'll spend hours chasing failures caused by shared state leaking between tests.
The four lifecycle annotations
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class DatabaseTest {
@BeforeAll
static void openConnection() {
System.out.println("1. @BeforeAll — once before ALL tests in this class");
// Open DB connection, start test server, etc.
}
@BeforeEach
void setUp() {
System.out.println("2. @BeforeEach — before EACH test method");
// Reset state, clear tables, create fresh test data
}
@Test
void firstTest() {
System.out.println("3. @Test — running firstTest");
assertTrue(true);
}
@Test
void secondTest() {
System.out.println("3. @Test — running secondTest");
assertTrue(true);
}
@AfterEach
void tearDown() {
System.out.println("4. @AfterEach — after EACH test method");
// Roll back DB transaction, close resources
}
@AfterAll
static void closeConnection() {
System.out.println("5. @AfterAll — once after ALL tests in this class");
// Close DB connection, stop test server
}
}Running this prints:
1. @BeforeAll — once before ALL tests in this class
2. @BeforeEach — before EACH test method
3. @Test — running firstTest
4. @AfterEach — after EACH test method
2. @BeforeEach — before EACH test method
3. @Test — running secondTest
4. @AfterEach — after EACH test method
5. @AfterAll — once after ALL tests in this class
The @BeforeEach / @AfterEach pair brackets every single test. @BeforeAll / @AfterAll fire once for the whole class. This is the execution contract — memorise it and it will never surprise you.
The static constraint on @BeforeAll and @AfterAll
@BeforeAll and @AfterAll must be static by default. The reason: JUnit 5 creates a new instance of the test class for every test method (the default behaviour). If @BeforeAll were non-static, each instance would call it — defeating the "run once" purpose.
This is different from TestNG, which creates one instance of the test class and reuses it. JUnit's per-method instance model makes tests more isolated — instance state can't accidentally leak from one test to the next. The trade-off is the static requirement.
@TestInstance — removing the static requirement
When you genuinely need to share instance state between lifecycle methods (for example, a WebDriver instance that you initialize in @BeforeAll and inject via a ParameterResolver in Chapter 4), use @TestInstance(TestInstance.Lifecycle.PER_CLASS):
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SeleniumTest {
WebDriver driver;
@BeforeAll
void startBrowser() {
// Non-static! Works because PER_CLASS creates one instance
driver = new ChromeDriver();
}
@Test
void loginPage() {
driver.get("https://example.com/login");
assertNotNull(driver.findElement(By.id("email")));
}
@AfterAll
void quitBrowser() {
// Non-static! Same instance as @BeforeAll
driver.quit();
}
}With PER_CLASS, JUnit creates one instance for the whole class. Non-static @BeforeAll and @AfterAll work, and @BeforeEach still runs before each test on that same instance. The catch: tests can now share state through instance fields, which means test order can matter. Use PER_CLASS deliberately, not as a shortcut to avoid static.
Mapping to TestNG
If you know TestNG, the mapping is straightforward:
| TestNG | JUnit 5 |
|---|---|
@BeforeMethod | @BeforeEach |
@AfterMethod | @AfterEach |
@BeforeClass | @BeforeAll (static) |
@AfterClass | @AfterAll (static) |
@BeforeSuite | No direct equivalent — use extensions or suite configuration |
@BeforeTest | No direct equivalent — use @BeforeAll per class or extensions |
TestNG's @BeforeSuite fires once across the entire test suite. JUnit 5 has no direct equivalent at the class level — to run code once across multiple test classes, you need a JUnit Platform LauncherSessionListener or the @BeforeAll on a base class with @TestInstance(PER_CLASS). For most QA use cases, per-class @BeforeAll is sufficient.
QA patterns for lifecycle methods
WebDriver setup (PER_CLASS approach):
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class LoginPageTest {
WebDriver driver;
@BeforeAll
void startBrowser() {
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(5));
}
@BeforeEach
void navigateToLogin() {
driver.get("https://example.com/login");
}
@Test
void shouldLoginWithValidCredentials() {
driver.findElement(By.id("email")).sendKeys("alice@test.com");
driver.findElement(By.id("password")).sendKeys("secret");
driver.findElement(By.cssSelector("button[type='submit']")).click();
assertEquals("https://example.com/dashboard", driver.getCurrentUrl());
}
@AfterAll
void closeBrowser() {
if (driver != null) driver.quit();
}
}The lifecycle flow
Step 1 of 5
@BeforeAll fires once
@BeforeAll (static by default) runs once before any test method in the class. Use it for expensive setup: starting a server, opening a database connection, creating a ChromeDriver. This runs exactly once regardless of how many @Test methods exist.
⚠️ Common mistakes
- Non-static
@BeforeAllwithout@TestInstance(PER_CLASS). This throwsorg.junit.jupiter.api.extension.ExtensionConfigurationException: @BeforeAll method must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS). The fix is either addingstaticor adding the annotation — choose based on whether you need shared instance state. - Using
@BeforeMethod— the TestNG name.@BeforeMethodis not a JUnit 5 annotation. It compiles only if you have TestNG on the classpath, in which case JUnit won't see it at all. The JUnit 5 name is@BeforeEach. Check imports if lifecycle methods don't seem to be running. - Sharing mutable state through instance fields without
PER_CLASS. By default, each test gets a fresh instance. If you setthis.user = new User(...)in@BeforeEachand read it in@Test, that works because@BeforeEachruns on the same instance as@Test. But if you setthis.userin one@Testmethod and read it in another, you're reading a field on a different instance — you'll getnull. If tests need to share state, usePER_CLASSdeliberately.
🎯 Practice task
Build a lifecycle-conscious test class. 20–25 minutes.
- Create a
UserRepositoryTestclass with a simple in-memoryUserRepository(a class with aList<User>andadd/findByEmail/clearmethods). - Add
@BeforeAll static void initRepository()that instantiates the repository and stores it in astaticfield. - Add
@BeforeEach void seedData()that adds three test users to the repository. - Add
@AfterEach void clearData()that callsrepository.clear(). - Write three
@Testmethods: find a user by email, find a non-existent user, and add a duplicate email that should throw. Runmvn testand verify that each test starts from the seeded state (not accumulating data from previous tests). - Observe the isolation. Add
System.out.println(repository.size())at the start of each@Test. Confirm it always prints3(the seed size), proving@AfterEachcleared and@BeforeEachre-seeded between tests. - Stretch — convert to PER_CLASS. Add
@TestInstance(Lifecycle.PER_CLASS)and make@BeforeAllnon-static. Confirm the tests still pass with no other changes.
Next chapter: assertions in depth — assertEquals, assertThrows, assertAll, assertTimeout, and the full comparison toolkit.