← 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