Exit course
Chapter 4 of 11 Intermediate 7 min read 3 quick checks Updated

TestNG annotations and the driver lifecycle

On this page
  1. The execution order
  2. Where should the browser be created?
  3. alwaysRun and groups
  4. Moving the lifecycle into a base class
  5. Injecting context into configuration methods
  6. @Test attributes worth knowing

TestNG has ten configuration annotations, and which one you use to create and quit the browser decides how isolated, how fast and how parallel-safe your suite is. This chapter maps them out, then moves the driver lifecycle into a base class.

The execution order#

For a suite with one <test> containing one class with two test methods, TestNG runs:

@BeforeSuite          once per suite
  @BeforeTest         once per <test> block in testng.xml
    @BeforeClass      once per test class
      @BeforeMethod   before each @Test method
        @Test         testA
      @AfterMethod
      @BeforeMethod
        @Test         testB
      @AfterMethod
    @AfterClass
  @AfterTest
@AfterSuite

There are also @BeforeGroups and @AfterGroups, which run before the first and after the last test method of a named group.

Two details matter more than the diagram suggests:

  • @BeforeTest is about the <test> tag in testng.xml, not about test methods. People regularly put driver setup in @BeforeTest expecting it to run per test method, then find that every method in the <test> block shares one browser.
  • Inheritance order. @Before... methods in a superclass run before those in a subclass; @After... methods run in reverse (subclass first, then superclass). That is what makes a base class safe: the browser exists before any subclass setup runs and is still alive during subclass teardown.

You can see the order yourself with a throwaway class:

package com.example;

import org.testng.annotations.*;

public class LifecycleDemoTest {
    @BeforeSuite  public void beforeSuite()  { System.out.println("BeforeSuite"); }
    @BeforeTest   public void beforeTest()   { System.out.println("  BeforeTest"); }
    @BeforeClass  public void beforeClass()  { System.out.println("    BeforeClass"); }
    @BeforeMethod public void beforeMethod() { System.out.println("      BeforeMethod"); }

    @Test public void testA() { System.out.println("        testA"); }
    @Test public void testB() { System.out.println("        testB"); }

    @AfterMethod  public void afterMethod()  { System.out.println("      AfterMethod"); }
    @AfterClass   public void afterClass()   { System.out.println("    AfterClass"); }
    @AfterTest    public void afterTest()    { System.out.println("  AfterTest"); }
    @AfterSuite   public void afterSuite()   { System.out.println("AfterSuite"); }
}

Run it with mvn test -Dtest=LifecycleDemoTest and compare the console output with the diagram. Note that TestNG does not promise an order between testA and testB; don't write tests that rely on one.

Where should the browser be created?#

Create in One browser per Isolation Speed Parallel-safe
@BeforeSuite whole run none: cookies, local storage and open dialogs leak between all tests fastest no
@BeforeClass test class tests in a class affect each other fast only with parallel="classes"
@BeforeMethod test method full slowest (a new session per test) yes, with ThreadLocal (chapter 7)

For UI tests, create the driver in @BeforeMethod and quit it in @AfterMethod. Starting a new session for every test costs time, more so on a remote grid, but in return every test starts clean, can run in any order, can run in parallel, and fails for its own reasons rather than because the previous test left a modal open. When a per-class browser does make sense (a long read-only walk through one page, say), use @BeforeClass deliberately and keep that class out of method-level parallelism.

Use @BeforeSuite and @BeforeClass for things that are expensive and safe to share: reading configuration, creating test data through an API, starting a local mock server.

One class walks through a long read-only report page with twelve checks and never changes state. When is a per-class browser in @BeforeClass reasonable?

A shared browser is acceptable when tests cannot affect each other, but its methods must not run in parallel. Making the driver static would share it across classes too.

alwaysRun and groups#

Configuration methods belong to groups too. If you run only the smoke group (chapter 6) and your @BeforeMethod has no group, TestNG skips it, and every smoke test fails with a NullPointerException on driver. Mark lifecycle methods with alwaysRun = true:

  • On a @Before... method it means "run regardless of which groups are selected".
  • On an @After... method it additionally means "run even if a previous configuration or test method failed", which is what guarantees quit() is called.

Moving the lifecycle into a base class#

Create BaseTest.java:

package com.example;

import java.time.Duration;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;

public abstract class BaseTest {

    private WebDriver driver;

    protected WebDriver driver() {
        return driver;
    }

    protected WebDriverWait waitUpTo(int seconds) {
        return new WebDriverWait(driver, Duration.ofSeconds(seconds));
    }

    @BeforeMethod(alwaysRun = true)
    public void startDriver() {
        ChromeOptions options = new ChromeOptions();
        if (Boolean.getBoolean("headless")) {
            options.addArguments("--headless=new", "--window-size=1920,1080");
        }
        driver = new ChromeDriver(options);
    }

    @AfterMethod(alwaysRun = true)
    public void stopDriver() {
        if (driver != null) {
            driver.quit();
            driver = null;
        }
    }
}

and slim the test down:

package com.example;

import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.testng.Assert;
import org.testng.annotations.Test;

public class FirstTest extends BaseTest {

    @Test
    public void submitsTheWebForm() {
        driver().get("https://www.selenium.dev/selenium/web/web-form.html");
        driver().findElement(By.id("my-text-id")).sendKeys("Selenium");
        driver().findElement(By.cssSelector("button[type='submit']")).click();

        String message = waitUpTo(10)
                .until(ExpectedConditions.visibilityOfElementLocated(By.id("message")))
                .getText();
        Assert.assertEquals(message, "Received!");
    }
}

Boolean.getBoolean("headless") reads a JVM system property, so mvn test -Dheadless=true switches to headless without a code change.

Why an accessor method driver() rather than a protected WebDriver driver field? Because in chapter 7 the driver moves into a ThreadLocal for parallel runs. With every test calling driver(), that change touches one file. With a public field, it touches every test.

Why does BaseTest expose the driver through a driver() method rather than a protected field?

Every test calls driver(), so the storage behind it can change for parallel runs without touching any test class.

Injecting context into configuration methods#

TestNG can pass useful objects into configuration methods if you declare them as parameters. Two you will use:

import java.lang.reflect.Method;
import org.testng.ITestResult;

@BeforeMethod(alwaysRun = true)
public void startDriver(Method method) {
    System.out.println("Starting browser for " + method.getName());
    // ...
}

@AfterMethod(alwaysRun = true)
public void stopDriver(ITestResult result) {
    System.out.println(result.getName() + " passed? " + result.isSuccess());
    // ...
}

Method gives you the test method about to run (useful for naming the session on a grid). ITestResult tells you how it ended (useful for reporting pass or fail to a grid). Chapter 9 uses both.

@Test attributes worth knowing#

@Test(description = "Form submission shows a confirmation",
      groups = {"smoke"},
      timeOut = 60_000,             // fail if the method runs longer than 60s
      enabled = true)               // false to switch a test off
public void submitsTheWebForm() { ... }

timeOut is a client-side safety net. Remote grids usually enforce their own limit as well; on TestingBot that is the maximum test duration option, which stops a hung session from running on.

priority also exists and orders methods within a class (lower runs first), but needing it is usually a sign that tests depend on each other. dependsOnMethods (chapter 6) states that dependency explicitly, and tests that need neither are the ones that parallelise cleanly.

The TestNG with Selenium article has a compact reference of every annotation if you want one page to bookmark.

Your tests only pass when you give them priority = 1, 2 and 3. What does that tell you?

Needing an order means one test relies on another's leftovers. Independent tests run in any order and in parallel.