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.baseURIand 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
@BeforeSuitein a class not listed intestng.xml. TestNG only executes lifecycle annotations for classes it knows about. IfGlobalSetupis missing from the<classes>list or not in a scanned<package>,@BeforeSuitenever fires. The suite starts,RestAssured.baseURIis null, and every API test fails withNullPointerException. Check the class listing first whenever@BeforeSuiteseems not to run. - Using static fields set in
@BeforeSuiteand read in@BeforeMethodin a parallel suite. If both the reader and writer are on different threads, you needvolatileor synchronisation on the shared field.@BeforeSuitealways runs on a single thread before parallelism starts, so the write is safe — but read carefully if a static field is mutated later. - Confusing
@BeforeTestscope with test method scope.@BeforeTestfires once per<test>XML block. If your suite has one<test>block,@BeforeTestlooks exactly like@BeforeSuitein the output. Add a second<test>block and it fires twice — surprising if you expected it to run once. When in doubt, add aSystem.out.printlnwith the scope label and run the suite to confirm actual firing order.
🎯 Practice task
Build the multi-scope lifecycle. 30–40 minutes.
- Create a
GlobalSetup.javaclass with@BeforeSuitethat setsRestAssured.baseURI(or prints a config message) and@AfterSuitethat logs "Suite complete." - Create a
BaseTest.javawith@BeforeTest @Parameters("browser")that reads the browser parameter and@BeforeMethodthat creates the driver. - Write
testng.xmlwith two<test>blocks — one for Chrome and one for Firefox, each with<parameter name="browser" value="..."/>. - Run the suite. Verify the console shows
[SUITE]once,[TEST BLOCK] chromethen[TEST BLOCK] firefoxas the blocks run, and[METHOD]wrapping each individual test. - Verify
@BeforeSuitefires before any class setup. AddSystem.out.println("[CLASS] setup")to a@BeforeClassin one of your test classes. Confirm[SUITE]always prints before[CLASS]. - 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.