You got an introduction to TestNG in the Selenium course — @BeforeMethod, @Test, @AfterMethod. This lesson covers the full picture: all ten lifecycle annotations, their exact execution order, and every @Test attribute worth knowing. Understanding this order is not academic — it determines where driver setup belongs, why a @BeforeClass in one class fires before a @BeforeMethod in another, and why "works solo but breaks in the suite" almost always means something landed at the wrong lifecycle level.
All ten annotations, in execution order
package com.mycompany.tests.tests;
import org.testng.annotations.*;
public class AnnotationOrderDemo {
@BeforeSuite
public void beforeSuite() { log("1 @BeforeSuite — once per entire run"); }
@BeforeTest
public void beforeTest() { log("2 @BeforeTest — once per <test> block in testng.xml"); }
@BeforeClass
public void beforeClass() { log("3 @BeforeClass — once per test class"); }
@BeforeMethod
public void beforeMethod() { log("4 @BeforeMethod — before EACH @Test method"); }
@Test(priority = 1)
public void firstTest() { log("5 @Test — first test"); }
@AfterMethod
public void afterMethod() { log("6 @AfterMethod — after EACH @Test method"); }
@Test(priority = 2)
public void secondTest() { log("7 @Test — second test (4 → 7 → 6 repeats)"); }
@AfterClass
public void afterClass() { log("8 @AfterClass — once per test class, after all methods"); }
@AfterTest
public void afterTest() { log("9 @AfterTest — once per <test> block"); }
@AfterSuite
public void afterSuite() { log("10 @AfterSuite — last thing that runs"); }
private void log(String msg) { System.out.println(msg); }
}Run this and the console prints exactly:
1 @BeforeSuite — once per entire run
2 @BeforeTest — once per <test> block in testng.xml
3 @BeforeClass — once per test class
4 @BeforeMethod — before EACH @Test method
5 @Test — first test
6 @AfterMethod — after EACH @Test method
4 @BeforeMethod — before EACH @Test method
7 @Test — second test (4 → 7 → 6 repeats)
6 @AfterMethod — after EACH @Test method
8 @AfterClass — once per test class, after all methods
9 @AfterTest — once per <test> block
10 @AfterSuite — last thing that runs
The pattern: outer scope wraps inner scope. @BeforeSuite wraps everything. @BeforeClass wraps all methods in the class. @BeforeMethod wraps each individual test. The After* annotations mirror the Before* in reverse.
Two annotations that trip everyone up
@BeforeTest is NOT "before a test method". It fires once per <test> block in testng.xml. If you have one <test> block, it fires once and looks like @BeforeSuite. Once you split into multiple <test> blocks (for cross-browser runs, environment splits), @BeforeTest fires once per block — between @BeforeSuite and @BeforeClass. Common real-world use: read the browser parameter from testng.xml in @BeforeTest and initialise the right driver factory for that block.
@BeforeClass runs once per class, not once per test. State you set up there is shared by every @Test method. That's a feature when you want shared login tokens for API tests; it's a bug when your first UI test leaves the browser in a logged-in state that your second test does not expect.
The lifecycle as a process
Step 1 of 6
@BeforeSuite
Fires once at the very start of the entire suite run. Load global config, start shared services, set up WebDriverManager. Expensive setup that never changes per-test belongs here.
Where Selenium setup belongs
package com.mycompany.tests.base;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
public class BaseTest {
protected WebDriver driver;
@BeforeMethod
public void setup() {
WebDriverManager.chromedriver().setup();
driver = new ChromeDriver();
driver.manage().window().maximize();
}
@AfterMethod(alwaysRun = true)
public void teardown() {
if (driver != null) driver.quit();
}
}Two decisions in this base class:
-
@BeforeMethodover@BeforeClassfor the driver. A fresh browser per test means no state leaks between tests — a test that ends on the wrong page, a cookie that shouldn't exist, a session that expired. The cost is speed: every test pays a full browser-startup cost (~2–3 seconds). That trade-off is almost always worth it for correctness. -
alwaysRun = trueon@AfterMethod. If@BeforeMethodfails (say, ChromeDriver can't connect),driverstays null and@AfterMethodwould NPE ondriver.quit().alwaysRun = truemeans teardown runs even when setup threw — and the null guard prevents the secondary NPE from masking the real failure.
Test classes extend BaseTest:
package com.mycompany.tests.tests;
import com.mycompany.tests.base.BaseTest;
import org.testng.Assert;
import org.testng.annotations.Test;
public class HomePageTest extends BaseTest {
@Test(description = "Verifies the page title contains the site name")
public void titleContainsSiteName() {
driver.get("https://qa.codes");
Assert.assertTrue(driver.getTitle().contains("qa.codes"), "Title mismatch");
}
@Test(description = "Verifies the navigation bar is present")
public void navigationBarIsVisible() {
driver.get("https://qa.codes");
Assert.assertTrue(
driver.findElement(org.openqa.selenium.By.tagName("nav")).isDisplayed()
);
}
}@Test attributes
@Test accepts several attributes that change individual test behaviour:
// Execution order — lower number runs first
@Test(priority = 1)
public void criticalPath() { ... }
// Skip this test without deleting it
@Test(enabled = false)
public void pendingUntilBugFixed() { ... }
// Appears in HTML reports — worth writing for every test
@Test(description = "Login with valid credentials lands on the dashboard")
public void loginHappyPath() { ... }
// Fail the test if it takes longer than 5 seconds
@Test(timeOut = 5000)
public void pageLoadsQuickly() { ... }
// Pass only if this specific exception is thrown
@Test(expectedExceptions = org.openqa.selenium.NoSuchElementException.class)
public void elementShouldNotExist() {
driver.findElement(org.openqa.selenium.By.id("ghost-element"));
}
// Run the same test 3 times — good for flake detection
@Test(invocationCount = 3)
public void shouldBeStable() { ... }
// Group this test — controls what runs in testng.xml
@Test(groups = {"smoke", "regression"})
public void coreFlow() { ... }
// Skip this test if loginTest fails
@Test(dependsOnMethods = {"loginTest"})
public void dashboardLoads() { ... }description is the one most teams skip and later regret. When a CI pipeline fails, the report shows method names like testFlow3. A one-sentence description turns a cryptic failure into a self-documenting one. Write it for every test.
expectedExceptions is the cleanest way to test error paths. Don't catch the exception inside the test and assert on it — that's fragile and verbose. Let TestNG handle it: the test passes when the exception is thrown, fails when it isn't.
⚠️ Common mistakes
- Confusing
@BeforeTestwith@BeforeMethod. They sound related; they're not.@BeforeTestfires at the<test>block level — once per XML block. In a suite with one<test>block it appears to run once before everything, which looks like@BeforeSuite. Split your suite into two<test>blocks and it suddenly fires twice — and something breaks. Be deliberate about which you mean. - Not using
alwaysRun = trueon teardown. If@BeforeMethodthrows (ChromeDriver binary not found, network down), TestNG marks the test as failed and moves to the next method — but it also skips@AfterMethodunless you setalwaysRun = true. Without the guard, leaked browser processes accumulate, and ifdriveris null, a baredriver.quit()throws NPE and pollutes the failure report. - Sharing state in
@BeforeClasswithout thinking about test order. TestNG does not guarantee method execution order within a class unless you usepriority. Two tests that both mutate the shareddriverinstance can interfere in ways that only manifest when another test runs first — the classic "passes locally, fails in CI" problem. When in doubt, use@BeforeMethod.
🎯 Practice task
Watch the lifecycle run live. 25–35 minutes.
- Add
AnnotationOrderDemoto your project. Run it via IntelliJ. Read every line of console output and match it to the numbered order in this lesson. Run it twice — confirm the order is identical both times. - Test
alwaysRun = true. InBaseTest, temporarily change@BeforeMethodtothrow new RuntimeException("Simulated setup failure"). Run one test. WithoutalwaysRun = trueon@AfterMethod, teardown is skipped and you'll see the driver (if it somehow started) not quit. AddalwaysRun = true. Run again — teardown now fires and the null guard prevents the NPE. Revert the exception. - Use
expectedExceptions. Add a test that navigates to a page and callsdriver.findElement(By.id("this-does-not-exist")). Annotate it@Test(expectedExceptions = NoSuchElementException.class). Run it — the test passes because the exception was expected. - Add
descriptionto three existing tests. Runmvn clean test, opentest-output/emailable-report.html, and confirm the descriptions appear in the report. invocationCountfor flake detection. Add@Test(invocationCount = 20)to any test you want to stress-test. Run it. Twenty identical invocations appear in the report — if any fail, the test is flaky.- Stretch —
priorityvs source order. Create a class with three@Testmethods in this source order:testC,testA,testB. Run withoutpriority— note the order TestNG chooses. Then addpriority = 1/2/3in order A→B→C. Run again and confirm the new order. Understand thatpriorityis the only guaranteed ordering mechanism.
Next lesson: testng.xml in depth. The suite file that ties every annotation, group, and parameter together.