@BeforeMethod, @AfterMethod, @BeforeClass, @AfterClass

8 min read

Of the ten TestNG lifecycle annotations, four do the majority of the work in real projects: @BeforeMethod, @AfterMethod, @BeforeClass, and @AfterClass. You got an introduction to the method-level pair in the Selenium course — this lesson covers both levels in depth, explains when to use each, and shows the third dimension that changes everything: @AfterMethod receiving the test result and acting on it. The difference between "each test gets a fresh browser" and "all tests share one browser" is a single annotation swap — but the consequences cascade through every test you write.

@BeforeMethod and @AfterMethod — per-test isolation

@BeforeMethod fires before every @Test method. @AfterMethod fires after every @Test method. Together they form a bracket that guarantees each test starts with a clean state.

package com.mycompany.tests.tests;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
 
public class LoginTest {
 
    WebDriver driver;
 
    @BeforeMethod
    public void setup() {
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver();
        driver.manage().window().maximize();
        driver.get("https://www.saucedemo.com");
    }
 
    @Test
    public void validLoginLandsOnInventory() {
        driver.findElement(By.id("user-name")).sendKeys("standard_user");
        driver.findElement(By.id("password")).sendKeys("secret_sauce");
        driver.findElement(By.id("login-button")).click();
        Assert.assertTrue(driver.getCurrentUrl().contains("/inventory.html"));
    }
 
    @Test
    public void invalidLoginShowsError() {
        driver.findElement(By.id("user-name")).sendKeys("bad_user");
        driver.findElement(By.id("password")).sendKeys("wrong_password");
        driver.findElement(By.id("login-button")).click();
        Assert.assertTrue(
            driver.findElement(By.cssSelector("[data-test='error']")).isDisplayed()
        );
    }
 
    @AfterMethod(alwaysRun = true)
    public void teardown() {
        if (driver != null) driver.quit();
    }
}

Every @Test in this class opens a fresh browser, navigates to the login page, and quits the browser when done. Neither test knows or cares what the other did. That isolation is the entire point.

alwaysRun = true on @AfterMethod ensures teardown runs even when @BeforeMethod throws. Without it: setup fails → TestNG marks test as failed → skips @AfterMethod → browser process leaks. The if (driver != null) guard handles the case where setup threw before the driver was assigned.

@BeforeClass and @AfterClass — shared setup for the class

@BeforeClass fires once before any @Test in the class runs. @AfterClass fires once after all @Test methods finish. They are not per-method — they wrap the whole class.

package com.mycompany.tests.tests;
 
import io.restassured.RestAssured;
import io.restassured.response.Response;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
 
public class ProductApiTest {
 
    String authToken;
    int createdProductId;
 
    @BeforeClass
    public void classSetup() {
        RestAssured.baseURI = "https://api.myapp.com";
        // Log in once — all tests in this class use the same token
        authToken = RestAssured
            .given().contentType("application/json")
            .body("{\"email\":\"admin@test.com\",\"password\":\"testpass\"}")
            .post("/auth/login")
            .jsonPath().getString("token");
        Assert.assertNotNull(authToken, "Auth token must not be null");
    }
 
    @Test
    public void canGetProductList() {
        Response response = RestAssured
            .given().header("Authorization", "Bearer " + authToken)
            .get("/products");
        Assert.assertEquals(response.statusCode(), 200);
    }
 
    @Test
    public void canCreateProduct() {
        Response response = RestAssured
            .given()
            .header("Authorization", "Bearer " + authToken)
            .contentType("application/json")
            .body("{\"name\":\"Test Widget\",\"price\":9.99}")
            .post("/products");
        Assert.assertEquals(response.statusCode(), 201);
        createdProductId = response.jsonPath().getInt("id");
    }
 
    @AfterClass(alwaysRun = true)
    public void classTeardown() {
        if (createdProductId > 0) {
            RestAssured
                .given().header("Authorization", "Bearer " + authToken)
                .delete("/products/" + createdProductId);
        }
    }
}

Login happens once. All four tests use the same authToken. This is the right choice for API tests where authentication is expensive (network round-trip) and the token is stateless (multiple tests can share it without interfering). The @AfterClass cleanup deletes data created during the test run.

@AfterMethod with ITestResult — act on the test outcome

@AfterMethod can receive the ITestResult object — the test result for the method that just ran. This is the standard pattern for automatic screenshot capture:

import org.testng.ITestResult;
import org.testng.annotations.AfterMethod;
 
@AfterMethod(alwaysRun = true)
public void afterMethod(ITestResult result) {
    if (result.getStatus() == ITestResult.FAILURE) {
        takeScreenshot(result.getName());
        System.out.println("Test FAILED: " + result.getName());
        System.out.println("Cause: " + result.getThrowable().getMessage());
    }
    if (driver != null) driver.quit();
}
 
private void takeScreenshot(String testName) {
    org.openqa.selenium.TakesScreenshot ts =
        (org.openqa.selenium.TakesScreenshot) driver;
    java.io.File screenshot = ts.getScreenshotAs(
        org.openqa.selenium.OutputType.FILE
    );
    try {
        java.nio.file.Files.copy(
            screenshot.toPath(),
            java.nio.file.Paths.get("screenshots/" + testName + ".png"),
            java.nio.file.StandardCopyOption.REPLACE_EXISTING
        );
    } catch (java.io.IOException e) {
        System.err.println("Could not save screenshot: " + e.getMessage());
    }
}

ITestResult.FAILURE, ITestResult.SUCCESS, and ITestResult.SKIP are the three status values you'll use most.

@BeforeMethod vs @BeforeClass — the decision

@BeforeMethod vs @BeforeClass — when to use each

@BeforeMethod

  • Runs before EACH @Test method

  • Fresh state per test — full isolation

  • Best for Selenium: new browser per test

  • Higher cost: N browsers for N tests

  • Survives test order changes

  • Default choice for UI automation

@BeforeClass

  • Runs ONCE before all @Test methods

  • Shared state across all tests in class

  • Best for API tests: shared auth token

  • Lower cost: one login for all tests

  • Fragile if tests mutate shared state

  • Default choice for stateless API tests

⚠️ Common mistakes

  • @BeforeClass for Selenium without thinking about state. A WebDriver created in @BeforeClass is shared by every test. Test 1 logs in, navigates to the cart, and fails. Test 2 now starts in the middle of the cart flow with leftover cookies and a non-login URL. The failure message points at test 2 but the root cause is test 1. Full isolation via @BeforeMethod eliminates this entire class of problem.
  • Omitting alwaysRun = true on teardown. TestNG's default is to skip @AfterMethod and @AfterClass when the test or setup threw. Without alwaysRun = true, a single setup failure causes all subsequent tests in the class to run without teardown — browser processes pile up, and the real root cause gets buried under secondary failures.
  • Not guarding driver.quit() with a null check. If @BeforeMethod throws before driver = new ChromeDriver() executes, driver is still null. A bare driver.quit() throws NPE, which TestNG reports as a teardown error — masking the original setup failure in the report. Always write if (driver != null) driver.quit().

🎯 Practice task

Experience both isolation levels directly. 25–35 minutes.

  1. Add LoginTest from this lesson to your project (using saucedemo.com or a site of your choice). Run it — both tests should pass, each getting its own browser.
  2. Observe isolation in action. Add a third test that navigates to a URL that requires login, but your @BeforeMethod only opens the home page. Run — the test fails correctly, not because of a previous test's state.
  3. Temporarily break isolation. Convert @BeforeMethod and @AfterMethod to @BeforeClass and @AfterClass. Now the driver is shared. Add a test that intentionally logs out. Run all three tests — the test after the logout test will fail because the session was destroyed. This is the state-leak bug in practice.
  4. Add ITestResult screenshot capture. Copy the afterMethod(ITestResult result) pattern into your BaseTest. Intentionally fail one test. Confirm a screenshot is saved to screenshots/.
  5. @BeforeClass for an API test. Create a simple ApiSetupTest that uses RestAssured (or java.net.http.HttpClient from the standard library) to make a GET request in @BeforeClass and store the result. Multiple @Test methods use the stored result. Confirm @BeforeClass runs once in the console output.
  6. Stretch — measure the speed difference. Use time mvn test with @BeforeMethod creating a browser, then switch to @BeforeClass. With 5 tests in a class, the @BeforeClass version should be about 4 browser-startups faster. Measure it. Understand the trade-off in concrete seconds.

Next lesson: @BeforeSuite, @AfterSuite, @BeforeTest, @AfterTest — the higher-scope annotations that manage whole suite setup and multi-block configuration.

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