You've been writing TestNG tests for four chapters now — @BeforeMethod, @Test, @AfterMethod. Time to understand the rest. TestNG defines ten lifecycle annotations that fire at different scopes (suite, test block, class, method) and run in a strict, predictable order. Knowing this order isn't trivia — it determines where your driver setup belongs, where shared state can safely live, and why "tests pass individually but fail in the suite" usually means you put something at the wrong level. This lesson walks the entire lifecycle, the practical placement rules for Selenium, and the @Test attributes that make individual test methods do more than just run.
The ten annotations
TestNG annotations come in pairs — Before* and After* — at four scopes:
public class TestLifecycleDemo {
@BeforeSuite public void beforeSuite() { System.out.println("Before SUITE"); }
@BeforeTest public void beforeTest() { System.out.println("Before TEST"); }
@BeforeClass public void beforeClass() { System.out.println("Before CLASS"); }
@BeforeMethod public void beforeMethod() { System.out.println("Before METHOD"); }
@Test public void testOne() { System.out.println("Test One"); }
@Test public void testTwo() { System.out.println("Test Two"); }
@AfterMethod public void afterMethod() { System.out.println("After METHOD"); }
@AfterClass public void afterClass() { System.out.println("After CLASS"); }
@AfterTest public void afterTest() { System.out.println("After TEST"); }
@AfterSuite public void afterSuite() { System.out.println("After SUITE"); }
}The execution order is mechanical. For the class above, the console prints:
Before SUITE
Before TEST
Before CLASS
Before METHOD
Test One
After METHOD
Before METHOD
Test Two
After METHOD
After CLASS
After TEST
After SUITE
Read it as nested scopes — suite wraps test wraps class wraps each method. Suite-level setup runs once across the entire run; method-level setup runs before every @Test. Two confusions worth pinning down on day one:
@BeforeTestis not "before a test method". It's before each<test>block intestng.xml. A<test>block is a curated group of classes — covered in lesson 2 of this chapter. If you don't have multiple<test>blocks,@BeforeTestruns once and behaves indistinguishably from@BeforeSuite.@BeforeClassruns once per class, not once per@Test. State you set up in@BeforeClassis shared by every test in the class. That's a feature when you want speed; it's a bug when one test corrupts state the next test needs.
The lifecycle, visualised
Step 1 of 6
@BeforeSuite
Runs ONCE at the very start of the entire test run. Right place for read-only suite setup like loading config or starting an API client.
Where Selenium things actually go
For Selenium tests, the placement rules are clear:
public class HomePageTest {
WebDriver driver;
WebDriverWait wait;
@BeforeMethod // fresh driver per @Test — full test isolation
public void setup() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
wait = new WebDriverWait(driver, Duration.ofSeconds(10));
driver.manage().window().maximize();
driver.get("https://qa.codes");
}
@AfterMethod // ALWAYS quit — leaks browser processes if you forget
public void teardown() {
if (driver != null) driver.quit();
}
}Two design decisions you'll make in every project:
@BeforeMethod(driver per test) vs@BeforeClass(driver per class). Per-method gives bulletproof isolation but is slow — each test pays a full browser-startup cost. Per-class is much faster but lets state leak between tests. Default to@BeforeMethod; switch to@BeforeClassonly when speed is measurably more important than isolation.@BeforeSuitefor things that genuinely cost real money to set up. A test database container started via Docker. A logged-in API client used as a setup-helper across the whole run. Don't put cheap things here just because "the suite runs once."
The @Test attributes worth knowing
@Test accepts attributes that change how a single test method behaves:
// Run in a specific order — lower number first. Useful when you accept ordering for speed.
@Test(priority = 1)
public void earlyTest() { ... }
// Skip — usually a temporary marker while a feature is unstable
@Test(enabled = false)
public void skippedForNow() { ... }
// Description appears in HTML reports — make it human
@Test(description = "Logs in with valid credentials and lands on the dashboard")
public void shouldLoginSuccessfully() { ... }
// Fail if the test takes longer than 5 seconds
@Test(timeOut = 5000)
public void shouldRespondQuickly() { ... }
// The test is EXPECTED to throw — fails if it doesn't
@Test(expectedExceptions = NoSuchElementException.class)
public void shouldThrowWhenElementMissing() { ... }
// Run the same test 3 times in a row — useful for flake hunting
@Test(invocationCount = 3)
public void shouldBeStableAcrossRetries() { ... }description deserves a special mention — most teams skip it, then read CI failure reports that show only method names like testInvalidLogin and wonder what they actually verified. A one-sentence description makes the report self-documenting.
expectedExceptions is the cleanest way to express "this should throw." Don't write try { ... } catch (...) { /* swallow */ } and call it passing — let TestNG handle it.
A complete lifecycle test
A class that demonstrates every annotation, run end to end:
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.*;
public class LifecycleDemoTest {
WebDriver driver;
@BeforeSuite
public void beforeSuite() {
System.out.println("[SUITE] Loading config, starting any session-wide services");
WebDriverManager.chromedriver().setup(); // resolve driver once for the whole run
}
@BeforeClass
public void beforeClass() {
System.out.println("[CLASS] Logging into a test API, seeding shared data");
}
@BeforeMethod
public void beforeMethod() {
System.out.println("[METHOD] Fresh driver");
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.get("https://qa.codes");
}
@Test(priority = 1, description = "Asserts the page title contains qa.codes")
public void shouldHaveQaCodesTitle() {
Assert.assertTrue(driver.getTitle().contains("qa.codes"));
}
@Test(priority = 2, description = "Asserts the Learn nav link is visible")
public void shouldHaveLearnLink() {
Assert.assertTrue(driver.findElement(By.linkText("Learn")).isDisplayed());
}
@AfterMethod
public void afterMethod() {
System.out.println("[METHOD] Quitting driver");
if (driver != null) driver.quit();
}
@AfterClass
public void afterClass() {
System.out.println("[CLASS] Tearing down class-level resources");
}
@AfterSuite
public void afterSuite() {
System.out.println("[SUITE] Reporting suite-level results");
}
}Run it. You'll see one [SUITE] line at the start and end, one [CLASS] at each end, and [METHOD] lines wrapping each @Test. Watching the order print live makes the lifecycle click in a way reading docs alone never does.
Comparison with JUnit and pytest
// JUnit 5 equivalents
@BeforeAll // = @BeforeClass (must be static in JUnit)
@BeforeEach // = @BeforeMethod
@AfterEach // = @AfterMethod
@AfterAll // = @AfterClass# pytest — fixtures with scope
@pytest.fixture(scope="session") # = @BeforeSuite
@pytest.fixture(scope="module") # ~= @BeforeTest
@pytest.fixture(scope="class") # = @BeforeClass
@pytest.fixture # = @BeforeMethod (default scope is function)TestNG has the most granular built-in lifecycle of the three. JUnit is similar in spirit but lacks the @BeforeSuite / @BeforeTest distinction. pytest is the most flexible because fixtures are values that can be combined arbitrarily, but you give up some of TestNG's declarative simplicity in return.
The Selenium tool entry covers driver-level concerns; the TestNG cheat sheet lists every annotation and @Test attribute.
⚠️ Common mistakes
- Putting driver creation in
@BeforeClass"for speed" without thinking through state leaks. Tests share the browser session — cookies, local storage, and any state from test #1 are visible to test #2. The first time it bites you is when an admin-only test passes locally and fails in CI because a previous test logged in as a regular user. - Confusing
@BeforeTestwith@BeforeMethod. They sound similar; they're not.@BeforeTestruns at the<test>block level intestng.xml. If your suite has one<test>block,@BeforeTestruns once and you may think it's running per method. Then you split into multiple<test>blocks and the behaviour suddenly changes. Be deliberate about which you mean. - Not putting
if (driver != null) driver.quit()in@AfterMethod. A failure in@BeforeMethod(e.g.,WebDriverManagercan't reach the network) leavesdriveras null. The unguardeddriver.quit()then NPEs in teardown — masking the real failure with a confusing secondary one. Always guard the quit.
🎯 Practice task
Drive the full lifecycle. 25–35 minutes.
- Add
LifecycleDemoTestfrom this lesson to your project. Run it. Read the console output and confirm the order matches the diagram. - Add a third test with
priority = 0. Run again. Watch howpriorityoverrides source order — the priority-0 test now runs first. - Try
expectedExceptions. Add a test that doesdriver.findElement(By.id("does-not-exist"))and is annotated@Test(expectedExceptions = NoSuchElementException.class). Run it. The test passes — because the exception was the expected outcome. @BeforeClassvs@BeforeMethodcost. Convert your@BeforeMethod/@AfterMethoddriver setup to@BeforeClass/@AfterClass. Time the suite withtime mvn test -Dtest=LifecycleDemoTest. Then convert back. The class-scope version is faster — but if your tests share state, it's a bug factory. Decide which trade-off fits your context.- Add
descriptionto every@Test. Runmvn clean test, opentarget/surefire-reports/index.html, and read the report. The descriptions appear in the report — no more wondering whattestFlow1actually checked. - Stretch —
invocationCountfor flake detection. Take any test you suspect might be flaky. Add@Test(invocationCount = 50). Run it. If even one of the 50 fails, the test is flaky. Bonus: addthreadPoolSize = 5to@Testand watch it run 50× across 5 threads — early preview of chapter 5's parallel-execution work.
Next lesson: testng.xml in depth. Suites, tests, classes, groups, dependencies — the configuration surface that turns a folder of test files into a controlled, repeatable, CI-runnable suite.