← Blog

"Selenium Series #7: TestNG — Annotations, Groups, Listeners and Parallel Execution"

TestNG is the test framework that powers enterprise Selenium suites. Learn every annotation, configure parallel execution, write listeners for screenshots on failure, and generate HTML reports.

reading now
views
comments

Series Navigation

Part 6: Waits — Implicit, Explicit and Fluent

Part 8: Page Object Model — Scalable Test Architecture


TestNG vs JUnit

Both are Java test frameworks. TestNG is preferred for Selenium because:

Feature TestNG JUnit 4
Parallel execution ✅ Built-in, configurable via XML ❌ Needs external plugins
Test grouping groups attribute ❌ Not supported
Data providers @DataProvider Limited with @Parameterized
Dependency between tests dependsOnMethods ❌ Not supported
Suite-level configuration ✅ testng.xml ❌ No equivalent
Listeners ✅ Powerful ITestListener Basic RunListener
Reports ✅ Built-in HTML reports External plugin needed

All TestNG Annotations

import org.testng.annotations.*;

public class AnnotationDemo {

    // ── SUITE LEVEL ──────────────────────────────────────────────────────────
    @BeforeSuite   // runs ONCE before any tests in the entire suite
    public void beforeSuite() {
        System.out.println("Suite starting — set up database, test data, etc.");
    }

    @AfterSuite    // runs ONCE after ALL tests in the suite finish
    public void afterSuite() {
        System.out.println("Suite finished — clean up shared resources");
    }

    // ── TEST LEVEL (within a <test> tag in testng.xml) ───────────────────────
    @BeforeTest    // runs once before all tests in a <test> block
    public void beforeTest() {
        System.out.println("Before test block");
    }

    @AfterTest     // runs once after all tests in a <test> block
    public void afterTest() {
        System.out.println("After test block");
    }

    // ── CLASS LEVEL ──────────────────────────────────────────────────────────
    @BeforeClass   // runs once before the first test method in this class
    public void beforeClass() {
        System.out.println("Class starting");
    }

    @AfterClass    // runs once after the last test method in this class
    public void afterClass() {
        System.out.println("Class finished");
    }

    // ── METHOD LEVEL ─────────────────────────────────────────────────────────
    @BeforeMethod  // runs before EACH test method
    public void beforeMethod() {
        System.out.println("Setting up browser");
    }

    @AfterMethod   // runs after EACH test method
    public void afterMethod() {
        System.out.println("Closing browser");
    }

    // ── TEST METHOD ──────────────────────────────────────────────────────────
    @Test
    public void myTest() {
        System.out.println("Running test");
    }
}

Execution order:

BeforeSuite
  BeforeTest
    BeforeClass
      BeforeMethod → @Test → AfterMethod  (repeated for each @Test)
      BeforeMethod → @Test → AfterMethod
    AfterClass
  AfterTest
AfterSuite

@Test Attributes

// Basic test
@Test
public void basicTest() { }

// Custom test name for reports
@Test(description = "Verify user can login with valid credentials")
public void loginTest() { }

// Expected exception — test passes only if this exception is thrown
@Test(expectedExceptions = IllegalArgumentException.class)
public void invalidInputTest() {
    throw new IllegalArgumentException("invalid");
}

// Expected exception with message
@Test(expectedExceptions = IllegalArgumentException.class,
      expectedExceptionsMessageRegExp = ".*invalid.*")
public void invalidInputWithMessageTest() { }

// Timeout — test fails if not done in 5 seconds
@Test(timeOut = 5000)
public void performanceTest() { }

// Groups — categorise tests
@Test(groups = {"smoke", "login"})
public void smokeLoginTest() { }

// Priority — lower number runs first (default is 0)
@Test(priority = 1)
public void firstTest() { }

@Test(priority = 2)
public void secondTest() { }

// Dependencies — run after another test
@Test(dependsOnMethods = {"loginTest"})
public void dashboardTest() { }

// Dependencies on groups
@Test(dependsOnGroups = {"login"})
public void postLoginTest() { }

// Skip this test
@Test(enabled = false)
public void skippedTest() { }

// Run this test N times
@Test(invocationCount = 3)
public void retryTest() { }

// Run with N parallel threads
@Test(invocationCount = 5, threadPoolSize = 3)
public void parallelInvocationTest() { }

Test Groups and Filtering

// Assign tests to groups
@Test(groups = {"smoke"})
public void verifyHomePageLoads() { }

@Test(groups = {"smoke", "regression"})
public void verifyLoginWorks() { }

@Test(groups = {"regression"})
public void verifyAdvancedSearch() { }

@Test(groups = {"slow", "regression"})
public void verifyReportGeneration() { }

Run specific groups in testng.xml:

<!-- Run only smoke tests -->
<suite name="Smoke Suite">
    <test name="Smoke Tests">
        <groups>
            <run>
                <include name="smoke"/>
            </run>
        </groups>
        <classes>
            <class name="com.yourname.selenium.tests.LoginTest"/>
            <class name="com.yourname.selenium.tests.HomeTest"/>
        </classes>
    </test>
</suite>

<!-- Run regression but exclude slow tests -->
<suite name="Regression Suite">
    <test name="Fast Regression">
        <groups>
            <run>
                <include name="regression"/>
                <exclude name="slow"/>
            </run>
        </groups>
        <packages>
            <package name="com.yourname.selenium.tests"/>
        </packages>
    </test>
</suite>

Parallel Execution in testng.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">

<!-- parallel="methods": each test METHOD runs in its own thread -->
<suite name="Parallel Suite" parallel="methods" thread-count="4">
    <test name="All Tests">
        <packages>
            <package name="com.yourname.selenium.tests"/>
        </packages>
    </test>
</suite>

<!-- parallel="classes": each test CLASS runs in its own thread -->
<suite name="Class Parallel Suite" parallel="classes" thread-count="3">
    <test name="All Tests">
        <packages>
            <package name="com.yourname.selenium.tests"/>
        </packages>
    </test>
</suite>

<!-- parallel="tests": each <test> block runs in its own thread -->
<suite name="Browser Parallel Suite" parallel="tests" thread-count="3">
    <test name="Chrome Tests">
        <parameter name="browser" value="chrome"/>
        <classes>
            <class name="com.yourname.selenium.tests.LoginTest"/>
        </classes>
    </test>
    <test name="Firefox Tests">
        <parameter name="browser" value="firefox"/>
        <classes>
            <class name="com.yourname.selenium.tests.LoginTest"/>
        </classes>
    </test>
    <test name="Edge Tests">
        <parameter name="browser" value="edge"/>
        <classes>
            <class name="com.yourname.selenium.tests.LoginTest"/>
        </classes>
    </test>
</suite>

Read the browser parameter in BaseTest:

@BeforeMethod
@Parameters("browser")
public void setUp(@Optional("chrome") String browser) {
    WebDriver driver = DriverFactory.createDriver(browser);
    driverThreadLocal.set(driver);
}

@DataProvider — Parameterised Tests

import org.testng.annotations.DataProvider;

public class LoginTest extends BaseTest {

    @DataProvider(name = "loginCredentials")
    public Object[][] provideLoginData() {
        return new Object[][] {
            // { username,       password,              expectedResult }
            { "tomsmith",       "SuperSecretPassword!", "success" },
            { "wronguser",      "wrongpass",            "failure" },
            { "tomsmith",       "wrongpass",            "failure" },
            { "",               "",                     "failure" },
        };
    }

    @Test(dataProvider = "loginCredentials",
          description = "Login with various credential combinations")
    public void testLoginCombinations(String username, String password, String expected) {
        WebDriver driver = getDriver();
        driver.get("https://the-internet.herokuapp.com/login");

        driver.findElement(By.id("username")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);
        driver.findElement(By.cssSelector("button[type='submit']")).click();

        WebElement flash = driver.findElement(By.id("flash"));

        if (expected.equals("success")) {
            Assert.assertTrue(flash.getText().contains("logged into a secure area"));
        } else {
            Assert.assertTrue(
                flash.getText().contains("invalid") ||
                flash.getText().contains("Your username is invalid"),
                "Should show error for: " + username + "/" + password
            );
        }
    }
}

ITestListener — Screenshots on Failure

// src/test/java/com/yourname/selenium/listeners/TestListener.java
package com.yourname.selenium.listeners;

import com.yourname.selenium.base.BaseTest;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.testng.*;

import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class TestListener implements ITestListener {

    @Override
    public void onTestFailure(ITestResult result) {
        // Get driver from the test instance
        Object testInstance = result.getInstance();
        if (testInstance instanceof BaseTest) {
            WebDriver driver = ((BaseTest) testInstance).getDriver();
            if (driver != null) {
                takeScreenshot(driver, result.getName());
            }
        }
    }

    private void takeScreenshot(WebDriver driver, String testName) {
        try {
            String timestamp = LocalDateTime.now()
                .format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
            String filename  = "FAIL_" + testName + "_" + timestamp + ".png";

            File src  = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
            File dest = new File("test-output/screenshots/" + filename);

            FileUtils.copyFile(src, dest);
            System.out.println("[SCREENSHOT] Saved: " + dest.getAbsolutePath());
        } catch (IOException e) {
            System.err.println("[SCREENSHOT] Failed to save: " + e.getMessage());
        }
    }

    @Override
    public void onTestStart(ITestResult result) {
        System.out.println("▶ Starting: " + result.getName());
    }

    @Override
    public void onTestSuccess(ITestResult result) {
        System.out.println("✓ Passed: " + result.getName());
    }

    @Override
    public void onTestSkipped(ITestResult result) {
        System.out.println("⊘ Skipped: " + result.getName());
    }

    @Override
    public void onStart(ITestContext context) {
        System.out.println("=== Suite Starting: " + context.getName() + " ===");
        new File("test-output/screenshots").mkdirs();
    }

    @Override
    public void onFinish(ITestContext context) {
        System.out.println("=== Suite Finished ===");
        System.out.println("Passed:  " + context.getPassedTests().size());
        System.out.println("Failed:  " + context.getFailedTests().size());
        System.out.println("Skipped: " + context.getSkippedTests().size());
    }
}

Register the listener in testng.xml:

<suite name="Full Suite">
    <listeners>
        <listener class-name="com.yourname.selenium.listeners.TestListener"/>
    </listeners>
    <test name="All Tests">
        <packages>
            <package name="com.yourname.selenium.tests"/>
        </packages>
    </test>
</suite>

What's Next

In Part 8 we build the Page Object Model — the design pattern that keeps Selenium code maintainable as your application and test suite grow. This is the most important architectural skill in Selenium.

Discussion

Loading...

Leave a Comment

All comments are reviewed before appearing. No links please.

0 / 1000