TestNG Annotations and Execution Order

9 min read

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:

  1. @BeforeMethod over @BeforeClass for 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.

  2. alwaysRun = true on @AfterMethod. If @BeforeMethod fails (say, ChromeDriver can't connect), driver stays null and @AfterMethod would NPE on driver.quit(). alwaysRun = true means 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 @BeforeTest with @BeforeMethod. They sound related; they're not. @BeforeTest fires 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 = true on teardown. If @BeforeMethod throws (ChromeDriver binary not found, network down), TestNG marks the test as failed and moves to the next method — but it also skips @AfterMethod unless you set alwaysRun = true. Without the guard, leaked browser processes accumulate, and if driver is null, a bare driver.quit() throws NPE and pollutes the failure report.
  • Sharing state in @BeforeClass without thinking about test order. TestNG does not guarantee method execution order within a class unless you use priority. Two tests that both mutate the shared driver instance 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.

  1. Add AnnotationOrderDemo to 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.
  2. Test alwaysRun = true. In BaseTest, temporarily change @BeforeMethod to throw new RuntimeException("Simulated setup failure"). Run one test. Without alwaysRun = true on @AfterMethod, teardown is skipped and you'll see the driver (if it somehow started) not quit. Add alwaysRun = true. Run again — teardown now fires and the null guard prevents the NPE. Revert the exception.
  3. Use expectedExceptions. Add a test that navigates to a page and calls driver.findElement(By.id("this-does-not-exist")). Annotate it @Test(expectedExceptions = NoSuchElementException.class). Run it — the test passes because the exception was expected.
  4. Add description to three existing tests. Run mvn clean test, open test-output/emailable-report.html, and confirm the descriptions appear in the report.
  5. invocationCount for 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.
  6. Stretch — priority vs source order. Create a class with three @Test methods in this source order: testC, testA, testB. Run without priority — note the order TestNG chooses. Then add priority = 1/2/3 in order A→B→C. Run again and confirm the new order. Understand that priority is the only guaranteed ordering mechanism.

Next lesson: testng.xml in depth. The suite file that ties every annotation, group, and parameter together.

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