Test Lifecycle — @BeforeAll, @BeforeEach, @AfterEach, @AfterAll

8 min read

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:

TestNGJUnit 5
@BeforeMethod@BeforeEach
@AfterMethod@AfterEach
@BeforeClass@BeforeAll (static)
@AfterClass@AfterAll (static)
@BeforeSuiteNo direct equivalent — use extensions or suite configuration
@BeforeTestNo 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 @BeforeAll without @TestInstance(PER_CLASS). This throws org.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 adding static or adding the annotation — choose based on whether you need shared instance state.
  • Using @BeforeMethod — the TestNG name. @BeforeMethod is 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 set this.user = new User(...) in @BeforeEach and read it in @Test, that works because @BeforeEach runs on the same instance as @Test. But if you set this.user in one @Test method and read it in another, you're reading a field on a different instance — you'll get null. If tests need to share state, use PER_CLASS deliberately.

🎯 Practice task

Build a lifecycle-conscious test class. 20–25 minutes.

  1. Create a UserRepositoryTest class with a simple in-memory UserRepository (a class with a List<User> and add / findByEmail / clear methods).
  2. Add @BeforeAll static void initRepository() that instantiates the repository and stores it in a static field.
  3. Add @BeforeEach void seedData() that adds three test users to the repository.
  4. Add @AfterEach void clearData() that calls repository.clear().
  5. Write three @Test methods: find a user by email, find a non-existent user, and add a duplicate email that should throw. Run mvn test and verify that each test starts from the seeded state (not accumulating data from previous tests).
  6. Observe the isolation. Add System.out.println(repository.size()) at the start of each @Test. Confirm it always prints 3 (the seed size), proving @AfterEach cleared and @BeforeEach re-seeded between tests.
  7. Stretch — convert to PER_CLASS. Add @TestInstance(Lifecycle.PER_CLASS) and make @BeforeAll non-static. Confirm the tests still pass with no other changes.

Next chapter: assertions in depth — assertEquals, assertThrows, assertAll, assertTimeout, and the full comparison toolkit.

// tip to track lessons you complete and pick up where you left off across devices.