@BeforeSuite, @AfterSuite, @BeforeTest, @AfterTest

8 min read

The method and class annotations cover most single-class test setups. Once your suite grows to multiple classes running across multiple environments — or you need shared infrastructure that is genuinely too expensive to create per-class — you need the higher-scope annotations: @BeforeSuite, @AfterSuite, @BeforeTest, and @AfterTest. This lesson makes the distinction concrete: what each annotation is actually scoped to, where it lives in the project, and the patterns that make cross-class setup reliable rather than fragile.

@BeforeSuite and @AfterSuite — once per entire run

@BeforeSuite fires once before any test class is instantiated. @AfterSuite fires once after the last test in the suite completes. These are the right place for setup that belongs to the entire run, not any individual class.

package com.mycompany.tests.base;
 
import io.restassured.RestAssured;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;
 
public class GlobalSetup {
 
    @BeforeSuite
    public void suiteSetup() {
        // Configure RestAssured globally — applies to every test class
        RestAssured.baseURI = System.getProperty("baseUrl", "https://staging.myapp.com");
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
 
        // Seed any reference data the entire suite depends on
        System.out.println("[SUITE] Starting — base URL: " + RestAssured.baseURI);
    }
 
    @AfterSuite
    public void suiteTeardown() {
        // Remove suite-level test data, generate summary logs
        System.out.println("[SUITE] Complete — all tests finished");
    }
}

This class does not need to be a test class itself — it just needs to be discoverable by TestNG (in the scanned package or listed in testng.xml). TestNG instantiates it, runs @BeforeSuite, then runs all the actual tests, then runs @AfterSuite.

Practical uses for @BeforeSuite:

  • Configure RestAssured.baseURI and default headers once
  • Call WebDriverManager.globalConfig() to set the cache path
  • Start a local mock server or WireMock stub
  • Seed reference data (countries, categories) into a test database

Do not put cheap setup here just because "the suite runs once." If a @BeforeClass handles it correctly, use @BeforeClass. @BeforeSuite is for things that are genuinely shared and expensive.

@BeforeTest and @AfterTest — once per XML block

@BeforeTest fires once before the first @Test method in a <test> block in testng.xml. @AfterTest fires once after the last @Test in that block finishes. One <test> block → one @BeforeTest / @AfterTest pair.

This is the natural home for cross-browser configuration — each <test> block represents a browser:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Cross Browser" parallel="tests" thread-count="2">
    <test name="Chrome Tests">
        <parameter name="browser" value="chrome"/>
        <classes>
            <class name="com.mycompany.tests.tests.LoginTest"/>
            <class name="com.mycompany.tests.tests.ProductTest"/>
        </classes>
    </test>
    <test name="Firefox Tests">
        <parameter name="browser" value="firefox"/>
        <classes>
            <class name="com.mycompany.tests.tests.LoginTest"/>
            <class name="com.mycompany.tests.tests.ProductTest"/>
        </classes>
    </test>
</suite>
package com.mycompany.tests.base;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Optional;
import org.testng.annotations.Parameters;
 
public class BaseTest {
 
    protected static ThreadLocal<String> browserName = new ThreadLocal<>();
 
    @BeforeTest
    @Parameters("browser")
    public void beforeTest(@Optional("chrome") String browser) {
        browserName.set(browser);
        System.out.println("[TEST BLOCK] Setting up for browser: " + browser);
 
        // Pre-resolve the driver binary for this browser type
        switch (browser.toLowerCase()) {
            case "firefox" -> WebDriverManager.firefoxdriver().setup();
            case "edge"    -> WebDriverManager.edgedriver().setup();
            default        -> WebDriverManager.chromedriver().setup();
        }
    }
}

Individual @BeforeMethod in BaseTest then reads browserName.get() and creates the right driver. The @BeforeTest just handles the one-time binary resolution for the block.

@Optional("chrome") provides a default value when running tests outside a suite (e.g., from IntelliJ on a single class) where no <parameter> is injected.

The full lifecycle across a multi-class suite

Step 1 of 7

@BeforeSuite

Fires once. GlobalSetup.suiteSetup() configures RestAssured, seeds reference data, calls WebDriverManager.globalConfig(). Only runs here — never repeated.

A concrete full-suite example

package com.mycompany.tests.base;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.ITestResult;
import org.testng.annotations.*;
 
public class BaseTest {
 
    private static final ThreadLocal<WebDriver> driverThread = new ThreadLocal<>();
    private static String suiteBaseUrl;
 
    protected WebDriver getDriver() { return driverThread.get(); }
 
    @BeforeSuite
    public void suiteSetup() {
        suiteBaseUrl = System.getProperty("baseUrl", "https://www.saucedemo.com");
        System.out.println("[SUITE] Base URL: " + suiteBaseUrl);
    }
 
    @BeforeTest
    @Parameters("browser")
    public void testSetup(@Optional("chrome") String browser) {
        System.out.println("[TEST BLOCK] Browser: " + browser);
    }
 
    @BeforeMethod
    @Parameters("browser")
    public void methodSetup(@Optional("chrome") String browser) {
        WebDriver driver = browser.equalsIgnoreCase("firefox")
            ? new FirefoxDriver()
            : new ChromeDriver();
        driver.manage().window().maximize();
        driver.get(suiteBaseUrl);
        driverThread.set(driver);
    }
 
    @AfterMethod(alwaysRun = true)
    public void methodTeardown(ITestResult result) {
        WebDriver driver = driverThread.get();
        if (result.getStatus() == ITestResult.FAILURE && driver != null) {
            System.out.println("[FAIL] Screenshot saved for: " + result.getName());
        }
        if (driver != null) {
            driver.quit();
            driverThread.remove();
        }
    }
 
    @AfterSuite
    public void suiteTeardown() {
        System.out.println("[SUITE] All tests complete");
    }
}

ThreadLocal<WebDriver> stores a driver per thread — essential when parallel="methods" or parallel="tests" is active. Each thread gets its own driver; they never share.

@BeforeSuite in a separate class

A common clean pattern: put @BeforeSuite in a dedicated GlobalSetup.java that has no @Test methods, and register it in testng.xml:

<suite name="Regression">
    <test name="All Tests">
        <classes>
            <class name="com.mycompany.tests.base.GlobalSetup"/>
            <class name="com.mycompany.tests.tests.LoginTest"/>
            <class name="com.mycompany.tests.tests.ProductTest"/>
        </classes>
    </test>
</suite>

TestNG runs @BeforeSuite from GlobalSetup before any test in any listed class. This keeps the infrastructure concern separate from the base test class.

⚠️ Common mistakes

  • Putting @BeforeSuite in a class not listed in testng.xml. TestNG only executes lifecycle annotations for classes it knows about. If GlobalSetup is missing from the <classes> list or not in a scanned <package>, @BeforeSuite never fires. The suite starts, RestAssured.baseURI is null, and every API test fails with NullPointerException. Check the class listing first whenever @BeforeSuite seems not to run.
  • Using static fields set in @BeforeSuite and read in @BeforeMethod in a parallel suite. If both the reader and writer are on different threads, you need volatile or synchronisation on the shared field. @BeforeSuite always runs on a single thread before parallelism starts, so the write is safe — but read carefully if a static field is mutated later.
  • Confusing @BeforeTest scope with test method scope. @BeforeTest fires once per <test> XML block. If your suite has one <test> block, @BeforeTest looks exactly like @BeforeSuite in the output. Add a second <test> block and it fires twice — surprising if you expected it to run once. When in doubt, add a System.out.println with the scope label and run the suite to confirm actual firing order.

🎯 Practice task

Build the multi-scope lifecycle. 30–40 minutes.

  1. Create a GlobalSetup.java class with @BeforeSuite that sets RestAssured.baseURI (or prints a config message) and @AfterSuite that logs "Suite complete."
  2. Create a BaseTest.java with @BeforeTest @Parameters("browser") that reads the browser parameter and @BeforeMethod that creates the driver.
  3. Write testng.xml with two <test> blocks — one for Chrome and one for Firefox, each with <parameter name="browser" value="..."/>.
  4. Run the suite. Verify the console shows [SUITE] once, [TEST BLOCK] chrome then [TEST BLOCK] firefox as the blocks run, and [METHOD] wrapping each individual test.
  5. Verify @BeforeSuite fires before any class setup. Add System.out.println("[CLASS] setup") to a @BeforeClass in one of your test classes. Confirm [SUITE] always prints before [CLASS].
  6. Stretch — simulate an expensive shared resource. In @BeforeSuite, "start" a mock HTTP server by recording a start time in a static field. In @AfterSuite, calculate and log the elapsed time. In @BeforeMethod, print the server's "address." This mirrors how teams use WireMock or a test database container.

Next lesson: test groups and dependencies — tagging tests for selective execution and creating prerequisite chains that skip downstream tests cleanly when something breaks.

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