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
@BeforeClassfor Selenium without thinking about state. AWebDrivercreated in@BeforeClassis 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@BeforeMethodeliminates this entire class of problem.- Omitting
alwaysRun = trueon teardown. TestNG's default is to skip@AfterMethodand@AfterClasswhen the test or setup threw. WithoutalwaysRun = 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@BeforeMethodthrows beforedriver = new ChromeDriver()executes,driveris still null. A baredriver.quit()throws NPE, which TestNG reports as a teardown error — masking the original setup failure in the report. Always writeif (driver != null) driver.quit().
🎯 Practice task
Experience both isolation levels directly. 25–35 minutes.
- Add
LoginTestfrom 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. - Observe isolation in action. Add a third test that navigates to a URL that requires login, but your
@BeforeMethodonly opens the home page. Run — the test fails correctly, not because of a previous test's state. - Temporarily break isolation. Convert
@BeforeMethodand@AfterMethodto@BeforeClassand@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. - Add
ITestResultscreenshot capture. Copy theafterMethod(ITestResult result)pattern into yourBaseTest. Intentionally fail one test. Confirm a screenshot is saved toscreenshots/. @BeforeClassfor an API test. Create a simpleApiSetupTestthat uses RestAssured (orjava.net.http.HttpClientfrom the standard library) to make a GET request in@BeforeClassand store the result. Multiple@Testmethods use the stored result. Confirm@BeforeClassruns once in the console output.- Stretch — measure the speed difference. Use
time mvn testwith@BeforeMethodcreating a browser, then switch to@BeforeClass. With 5 tests in a class, the@BeforeClassversion 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.