← All skills

Testng Skill

Unit testingJava

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill testng-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

TestNG — Advanced Implementation Playbook

§1 — Project Setup & Configuration

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>7.9.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>4.18.1</version>
    </dependency>
    <dependency>
        <groupId>io.github.bonigarcia</groupId>
        <artifactId>webdrivermanager</artifactId>
        <version>5.7.0</version>
    </dependency>
    <dependency>
        <groupId>io.qameta.allure</groupId>
        <artifactId>allure-testng</artifactId>
        <version>2.25.0</version>
    </dependency>
</dependencies>

<build><plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.2.5</version>
        <configuration>
            <suiteXmlFiles><suiteXmlFile>testng.xml</suiteXmlFile></suiteXmlFiles>
            <parallel>classes</parallel>
            <threadCount>4</threadCount>
            <argLine>-Denv=${env:qa}</argLine>
        </configuration>
    </plugin>
</plugins></build>

§2 — Suite XML Configuration

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Regression" parallel="classes" thread-count="4" verbose="1">
    <listeners>
        <listener class-name="listeners.RetryListener"/>
        <listener class-name="listeners.AllureListener"/>
        <listener class-name="listeners.ScreenshotListener"/>
    </listeners>

    <parameter name="browser" value="chrome"/>
    <parameter name="env" value="qa"/>

    <test name="Smoke">
        <groups>
            <run><include name="smoke"/></run>
        </groups>
        <classes>
            <class name="tests.LoginTest"/>
            <class name="tests.DashboardTest"/>
        </classes>
    </test>

    <test name="Regression">
        <groups>
            <run>
                <include name="regression"/>
                <exclude name="wip"/>
                <exclude name="flaky"/>
            </run>
        </groups>
        <packages><package name="tests.*"/></packages>
    </test>

    <test name="API" parallel="methods" thread-count="8">
        <classes><class name="tests.api.UserApiTest"/></classes>
    </test>
</suite>

Multi-Environment Suites

<!-- testng-staging.xml -->
<suite name="Staging">
    <parameter name="env" value="staging"/>
    <parameter name="browser" value="chrome"/>
    <suite-files>
        <suite-file path="testng-smoke.xml"/>
    </suite-files>
</suite>

§3 — BaseTest & Thread-Safe Driver

public class BaseTest {
    private static final ThreadLocal<WebDriver> driverThread = new ThreadLocal<>();
    protected WebDriver driver;

    @Parameters({"browser", "env"})
    @BeforeMethod(alwaysRun = true)
    public void setUp(@Optional("chrome") String browser, @Optional("qa") String env) {
        WebDriver d;
        switch (browser.toLowerCase()) {
            case "firefox":
                WebDriverManager.firefoxdriver().setup();
                d = new FirefoxDriver();
                break;
            case "edge":
                WebDriverManager.edgedriver().setup();
                d = new EdgeDriver();
                break;
            default:
                ChromeOptions opts = new ChromeOptions();
                if ("ci".equals(System.getenv("CI"))) {
                    opts.addArguments("--headless=new", "--no-sandbox", "--disable-gpu");
                }
                WebDriverManager.chromedriver().setup();
                d = new ChromeDriver(opts);
        }
        d.manage().window().maximize();
        d.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
        d.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));
        driverThread.set(d);
        driver = d;

        String baseUrl = ConfigReader.get(env + ".base.url");
        driver.get(baseUrl);
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown(ITestResult result) {
        if (result.getStatus() == ITestResult.FAILURE) {
            captureScreenshot(result.getName());
        }
        WebDriver d = driverThread.get();
        if (d != null) { d.quit(); driverThread.remove(); }
    }

    public WebDriver getDriver() { return driverThread.get(); }

    private void captureScreenshot(String testName) {
        try {
            File src = ((TakesScreenshot) getDriver()).getScreenshotAs(OutputType.FILE);
            String path = "screenshots/" + testName + "_" + System.currentTimeMillis() + ".png";
            FileUtils.copyFile(src, new File(path));
            Allure.addAttachment("Screenshot", new FileInputStream(new File(path)));
        } catch (Exception e) { /* log */ }
    }
}

Configuration Reader

public class ConfigReader {
    private static final Properties props = new Properties();

    static {
        String env = System.getProperty("env", "qa");
        try {
            props.load(ConfigReader.class.getResourceAsStream("/config-" + env + ".properties"));
        } catch (IOException e) { throw new RuntimeException(e); }
    }

    public static String get(String key) {
        return System.getProperty(key, props.getProperty(key));
    }
}

§4 — Data Providers (Advanced)

// Parallel DataProvider
@DataProvider(name = "loginData", parallel = true)
public Object[][] loginData() {
    return new Object[][] {
        {"admin@test.com", "admin123", true},
        {"user@test.com", "wrong", false},
        {"", "", false},
        {"sql@inject.com", "' OR 1=1 --", false},
    };
}

@Test(dataProvider = "loginData", groups = "smoke")
public void testLogin(String email, String password, boolean expected) {
    assertEquals(loginService.authenticate(email, password), expected);
}

// Excel DataProvider
@DataProvider(name = "excelData")
public Object[][] excelData() throws Exception {
    return ExcelReader.getData("testdata/users.xlsx", "Sheet1");
}

// JSON DataProvider
@DataProvider(name = "jsonData")
public Object[][] jsonData() throws Exception {
    List<Map<String, String>> data = JsonReader.readArray("testdata/cases.json");
    return data.stream().map(m -> new Object[]{m}).toArray(Object[][]::new);
}

// CSV DataProvider with Iterator (memory efficient)
@DataProvider(name = "csvData")
public Iterator<Object[]> csvData() throws Exception {
    CSVReader reader = new CSVReader(new FileReader("testdata/large-dataset.csv"));
    return reader.readAll().stream()
        .skip(1)  // skip header
        .map(row -> (Object[]) row)
        .iterator();
}

// Cross-class DataProvider
public class TestDataProviders {
    @DataProvider(name = "sharedData")
    public static Object[][] sharedData() {
        return new Object[][] { {"data1"}, {"data2"} };
    }
}

@Test(dataProvider = "sharedData", dataProviderClass = TestDataProviders.class)
public void testWithSharedData(String data) { /* ... */ }

§5 — Factory Pattern

public class DynamicLoginTest extends BaseTest {
    private String browser;
    private String resolution;

    @Factory(dataProvider = "browserMatrix")
    public DynamicLoginTest(String browser, String resolution) {
        this.browser = browser;
        this.resolution = resolution;
    }

    @DataProvider(name = "browserMatrix")
    public static Object[][] browserMatrix() {
        return new Object[][] {
            {"chrome", "1920x1080"},
            {"chrome", "1366x768"},
            {"firefox", "1920x1080"},
            {"edge", "1920x1080"},
        };
    }

    @Test(groups = "crossBrowser")
    public void testLoginRendering() {
        // each Factory instance runs as independent test
        driver.manage().window().setSize(parseDimension(resolution));
        // ... assert login page renders correctly
    }
}

§6 — Listeners (Production Suite)

// Retry Analyzer — configurable
public class RetryAnalyzer implements IRetryAnalyzer {
    private int count = 0;
    private static final int MAX = Integer.parseInt(
        System.getProperty("retry.max", "2"));

    @Override
    public boolean retry(ITestResult result) {
        if (count < MAX) { count++; return true; }
        return false;
    }
}

// Global retry via transformer
public class RetryListener implements IAnnotationTransformer {
    @Override
    public void transform(ITestAnnotation annotation, Class testClass,
                          Constructor ctor, Method method) {
        if (annotation.getRetryAnalyzerClass() == null) {
            annotation.setRetryAnalyzer(RetryAnalyzer.class);
        }
    }
}

// Screenshot + Allure on failure
public class ScreenshotListener implements ITestListener {
    @Override
    public void onTestFailure(ITestResult result) {
        Object instance = result.getInstance();
        if (instance instanceof BaseTest) {
            WebDriver driver = ((BaseTest) instance).getDriver();
            if (driver != null) {
                byte[] screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
                Allure.addAttachment("Failure Screenshot", "image/png",
                    new ByteArrayInputStream(screenshot), "png");
            }
        }
    }

    @Override
    public void onTestStart(ITestResult result) {
        Log.info("Starting: " + result.getMethod().getMethodName());
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        Log.info("Passed: " + result.getMethod().getMethodName() +
                 " in " + (result.getEndMillis() - result.getStartMillis()) + "ms");
    }
}

// Execution timing listener
public class TimingListener implements ISuiteListener {
    @Override
    public void onStart(ISuite suite) {
        suite.setAttribute("startTime", System.currentTimeMillis());
    }

    @Override
    public void onFinish(ISuite suite) {
        long start = (long) suite.getAttribute("startTime");
        long duration = System.currentTimeMillis() - start;
        Log.info("Suite completed in " + duration + "ms");
        Log.info("Passed: " + suite.getAllMethods().stream()
            .filter(m -> m.getCurrentInvocationCount() > 0).count());
    }
}

§7 — Soft Assertions & Dependencies

// Soft assertions — report ALL failures
@Test
public void testUserProfile() {
    SoftAssert soft = new SoftAssert();
    User user = userService.getUser(1);
    soft.assertEquals(user.getName(), "Alice", "Name mismatch");
    soft.assertEquals(user.getEmail(), "alice@test.com", "Email mismatch");
    soft.assertTrue(user.isActive(), "User should be active");
    soft.assertNotNull(user.getCreatedAt(), "Created date missing");
    soft.assertAll();
}

// Method dependencies
@Test(priority = 1, groups = "crud")
public void createUser() { userId = userService.create(testUser); }

@Test(priority = 2, dependsOnMethods = "createUser", groups = "crud")
public void verifyUser() { assertNotNull(userService.get(userId)); }

@Test(priority = 3, dependsOnMethods = "createUser", groups = "crud")
public void deleteUser() { userService.delete(userId); }

// Group dependencies
@Test(groups = "setup")
public void seedDatabase() { /* ... */ }

@Test(dependsOnGroups = "setup", groups = "regression")
public void testWithSeededData() { /* ... */ }

§8 — Page Object Integration

public class LoginPage {
    private final WebDriver driver;
    private final WebDriverWait wait;

    @FindBy(id = "email") private WebElement emailInput;
    @FindBy(id = "password") private WebElement passwordInput;
    @FindBy(css = "[data-testid='login-btn']") private WebElement loginButton;
    @FindBy(css = ".error-message") private WebElement errorMessage;

    public LoginPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        PageFactory.initElements(driver, this);
    }

    public DashboardPage loginAs(String email, String password) {
        emailInput.clear();
        emailInput.sendKeys(email);
        passwordInput.clear();
        passwordInput.sendKeys(password);
        loginButton.click();
        return new DashboardPage(driver);
    }

    public String getErrorMessage() {
        wait.until(ExpectedConditions.visibilityOf(errorMessage));
        return errorMessage.getText();
    }
}

// Test using Page Object
public class LoginTest extends BaseTest {
    @Test(groups = "smoke", dataProvider = "validLogins")
    public void testSuccessfulLogin(String email, String password) {
        LoginPage loginPage = new LoginPage(driver);
        DashboardPage dashboard = loginPage.loginAs(email, password);
        assertTrue(dashboard.isLoaded(), "Dashboard should load after login");
    }
}

§9 — Parallel Execution Strategies

<!-- Method-level: fastest, requires thread-safe tests -->
<suite parallel="methods" thread-count="8">

<!-- Class-level: each class in its own thread (recommended) -->
<suite parallel="classes" thread-count="4">

<!-- Test-level: each <test> block in its own thread -->
<suite parallel="tests" thread-count="3">

<!-- Instance-level: with @Factory -->
<suite parallel="instances" thread-count="4">

<!-- Mixed: different parallelism per test block -->
<suite parallel="tests" thread-count="3">
    <test name="UITests" parallel="classes" thread-count="2">
        <classes>...</classes>
    </test>
    <test name="APITests" parallel="methods" thread-count="8">
        <classes>...</classes>
    </test>
</suite>

Thread-Safe Guidelines

// Thread-safe: Use ThreadLocal for shared resources
private static final ThreadLocal<WebDriver> driver = new ThreadLocal<>();
private static final ThreadLocal<SoftAssert> softAssert = new ThreadLocal<>();

@BeforeMethod
public void init() {
    driver.set(new ChromeDriver());
    softAssert.set(new SoftAssert());
}

§10 — Reporting Integration

// Allure annotations
@Epic("User Management")
@Feature("Login")
@Story("Successful Login")
@Severity(SeverityLevel.CRITICAL)
@Description("Verifies that valid credentials allow login")
@Test(groups = "smoke")
public void testSuccessfulLogin() {
    Allure.step("Navigate to login page", () -> driver.get(baseUrl + "/login"));
    Allure.step("Enter credentials", () -> {
        loginPage.enterEmail("admin@test.com");
        loginPage.enterPassword("admin123");
    });
    Allure.step("Submit and verify", () -> {
        loginPage.clickLogin();
        assertTrue(dashboard.isLoaded());
    });
}

// ExtentReports listener
public class ExtentListener implements ITestListener {
    private static ExtentReports extent;
    private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();

    @Override
    public void onStart(ITestContext context) {
        extent = new ExtentReports();
        extent.attachReporter(new ExtentSparkReporter("reports/extent-report.html"));
    }

    @Override
    public void onTestStart(ITestResult result) {
        test.set(extent.createTest(result.getMethod().getMethodName()));
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        test.get().pass("Passed");
    }

    @Override
    public void onTestFailure(ITestResult result) {
        test.get().fail(result.getThrowable());
    }

    @Override
    public void onFinish(ITestContext context) { extent.flush(); }
}

§11 — CI/CD Integration

# GitHub Actions
name: TestNG Suite
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        suite: [testng-smoke.xml, testng-regression.xml]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: 17 }
      - run: mvn test -DsuiteXmlFile=${{ matrix.suite }} -Denv=ci
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results-${{ matrix.suite }}
          path: |
            test-output/
            target/surefire-reports/
            allure-results/
# Jenkins Pipeline
pipeline {
    agent any
    parameters {
        choice(name: 'SUITE', choices: ['smoke', 'regression', 'full'], description: 'Test suite')
        choice(name: 'ENV', choices: ['qa', 'staging', 'prod'], description: 'Environment')
    }
    stages {
        stage('Test') {
            steps {
                sh "mvn test -DsuiteXmlFile=testng-${params.SUITE}.xml -Denv=${params.ENV}"
            }
            post {
                always {
                    testNG(reportFilenamePattern: '**/testng-results.xml')
                    allure includeProperties: false, jdk: '', results: [[path: 'allure-results']]
                }
            }
        }
    }
}

§12 — Debugging Quick-Reference

ProblemCauseFix
Test not found/runningMissing @Test or wrong suite XML pathVerify annotations, check <classes> in XML
NullPointerException in parallelShared mutable state across threadsUse ThreadLocal for driver and data
DependencyExceptionDependent method failed or not foundCheck method name spelling, ensure dependency runs first
DataProvider returns 0 rowsFile path wrong or empty datasetVerify file path relative to resources, check data format
@BeforeMethod not runningMissing alwaysRun = trueAdd alwaysRun = true for grouped tests
Retry runs infinitelyCounter not per-instanceEnsure count resets per test (instance variable)
Groups not filteringWrong group name or XML configCheck <include> names match @Test(groups = ...)
Listener not triggeredNot registered in XML or @ListenersAdd to <listeners> in testng.xml
Flaky parallel testsRace conditions on shared resourcesUse ThreadLocal, avoid static mutable state
Screenshots missingDriver already quit in @AfterMethodCapture screenshot before driver.quit()
@Factory tests not independentShared state between factory instancesEach Factory instance should be fully independent
Allure report emptyMissing allure-testng dependency or listenerAdd dependency + register AllureTestNg listener

§13 — Best Practices Checklist

  • ✅ Use ThreadLocal<WebDriver> for parallel-safe driver management
  • ✅ Use @DataProvider(parallel = true) for data-driven parallelism
  • ✅ Use alwaysRun = true on setup/teardown with groups
  • ✅ Use SoftAssert when multiple independent checks needed
  • ✅ Use groups (smoke, regression, wip) for selective execution
  • ✅ Use listeners (not base classes) for cross-cutting concerns
  • ✅ Use @Factory for cross-browser / cross-config matrix tests
  • ✅ Use Allure or ExtentReports for rich HTML reports
  • ✅ Configure retry analyzer with env-configurable max count
  • ✅ Keep dependsOnMethods minimal — prefer independent tests
  • ✅ Use @Parameters for suite-level config (browser, env)
  • ✅ Use ConfigReader pattern for multi-environment properties
  • ✅ Store screenshots and logs as Allure attachments
  • ✅ Structure: tests/, pages/, listeners/, utils/, testdata/