TestNG listeners let your framework react to what happens during a run: take a screenshot when a test fails, log each test to Extent Reports, retry a flaky test, or change annotations before the run starts. This guide covers the listener interfaces, a complete ITestListener with Extent Reports and screenshots, the three ways to register listeners, and automatic retries with IRetryAnalyzer.

The TestNG Listener Interfaces

Listeners are interfaces in TestNG that allow you to listen to test execution events and take action. They hook into the test lifecycle — when a test starts, passes, fails, skips, or the suite starts/ends.

Listener InterfaceKey MethodsCommon Use
ITestListeneronTestStart, onTestSuccess, onTestFailure, onTestSkippedScreenshot on fail, ExtentReports logging, Slack notification
ISuiteListeneronStart(ISuite), onFinish(ISuite)Suite-level reporting, DB logging
ITestResult (not a listener)getStatus, getName, getThrowable, getStartMillisAccess test result details inside listeners
IReportergenerateReport(List<XmlSuite>, List<ISuite>, String outputDirectory)Generate custom HTML/XML/JSON reports
IRetryAnalyzerretry(ITestResult) — return true to retryAutomatically retry flaky tests
IAnnotationTransformertransform(ITestAnnotation, ...) — modify annotationsProgrammatically add retryAnalyzer to all tests
IInvokedMethodListenerbeforeInvocation, afterInvocationIntercept every method call (before/after each test/config)
IAlterSuiteListeneralter(List<XmlSuite>)Modify test suite programmatically at runtime
Advertisement

ITestListener: A Complete Implementation

import org.testng.*;
import com.aventstack.extentreports.*;
public class TestListener implements ITestListener {
    private static ExtentReports extent = ExtentManager.getInstance();
    private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();
    @Override
    public void onStart(ITestContext context) {
        System.out.println("<test> started: " + context.getName());
    }
    @Override
    public void onTestStart(ITestResult result) {
        ExtentTest extentTest = extent.createTest(
            result.getMethod().getMethodName(),
            result.getMethod().getDescription()
        );
        test.set(extentTest);
        test.get().info("Test Started: " + result.getName());
    }
    @Override
    public void onTestSuccess(ITestResult result) {
        test.get().pass("Test PASSED");
    }
    @Override
    public void onTestFailure(ITestResult result) {
        test.get().fail(result.getThrowable());
        // Capture screenshot
        WebDriver driver = DriverFactory.getDriver();
        if (driver != null) {
            String screenshotPath = ScreenshotUtil.capture(driver, result.getName());
            test.get().addScreenCaptureFromPath(screenshotPath);
        }
    }
    @Override
    public void onTestSkipped(ITestResult result) {
        test.get().skip("Test SKIPPED: " + result.getThrowable());
    }
    @Override
    public void onFinish(ITestContext context) {
        extent.flush();  // write report to file
        System.out.println("Passed:  " + context.getPassedTests().size());
        System.out.println("Failed:  " + context.getFailedTests().size());
        System.out.println("Skipped: " + context.getSkippedTests().size());
    }
}

ExtentTest is kept in a ThreadLocal so parallel tests don't write into each other's report entry, and DriverFactory.getDriver() returns the current thread's browser for the screenshot.

Registering Listeners: Three Ways

<suite name="Suite">
    <listeners>
        <listener class-name="listeners.TestListener"/>
        <listener class-name="listeners.RetryListener"/>
    </listeners>
    ...
</suite>

Method 2 — @Listeners annotation on test class

@Listeners({TestListener.class, RetryListener.class})
public class LoginTest extends BaseTest {
    @Test
    public void testLogin() { ... }
}

Method 3 — ServiceLoader (global — applies to ALL suites)

// Create file: src/test/resources/META-INF/services/org.testng.ITestNGListener
// Content of file (one class per line):
listeners.TestListener
listeners.RetryListener
// This listener applies globally — no XML or annotation needed

RetryAnalyzer: Retrying Flaky Tests

import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
public class RetryAnalyzer implements IRetryAnalyzer {
    private int retryCount = 0;
    private static final int MAX_RETRY = 2;  // retry up to 2 times
    @Override
    public boolean retry(ITestResult result) {
        if (retryCount < MAX_RETRY) {
            retryCount++;
            System.out.println("Retrying: " + result.getName()
                + " | Attempt: " + retryCount + " of " + MAX_RETRY);
            return true;   // retry this test
        }
        return false;      // no more retries
    }
}
// Apply to specific test
@Test(retryAnalyzer = RetryAnalyzer.class)
public void testFlakyLoginButton() { ... }
// ── Apply RetryAnalyzer to ALL tests automatically ─────────────────────
// Use IAnnotationTransformer — no need to add retryAnalyzer to every @Test
public class RetryListener implements IAnnotationTransformer {
    @Override
    public void transform(ITestAnnotation annotation, Class testClass,
                          Constructor testConstructor, Method testMethod) {
        annotation.setRetryAnalyzer(RetryAnalyzer.class);
        // This adds RetryAnalyzer to EVERY @Test automatically
    }
}
// Register RetryListener in testng.xml:
// <listener class-name="listeners.RetryListener"/>
Retry Count in Reports

When a test retries, it is shown in the report as 'Skipped' for the retry attempts and 'Failed' if all retries fail.

To show only the final result, implement IReporter to filter intermediate retry results from the report.

Use retries to keep a pipeline moving while you fix flaky tests, not as a fix in themselves: report how often each test needed a retry, and investigate any that retry regularly.

Extent Reports with TestNG

<!-- pom.xml -->
<dependency>
    <groupId>com.aventstack</groupId>
    <artifactId>extentreports</artifactId>
    <version>5.1.1</version>
</dependency>
// ExtentManager.java — Singleton
public class ExtentManager {
    private static ExtentReports extent;
    public static ExtentReports getInstance() {
        if (extent == null) {
            ExtentSparkReporter spark = new ExtentSparkReporter("reports/TestReport.html");
            spark.config().setDocumentTitle("Automation Test Report");
            spark.config().setReportName("Regression Suite");
            spark.config().setTheme(Theme.DARK);
            extent = new ExtentReports();
            extent.attachReporter(spark);
            extent.setSystemInfo("OS", System.getProperty("os.name"));
            extent.setSystemInfo("Java", System.getProperty("java.version"));
        }
        return extent;
    }
}

FAQs

What is a listener in TestNG?

An interface whose methods TestNG calls on events during a run, such as a test starting, passing, failing or being skipped. ITestListener is the most used, for screenshots and report logging.

How do you take a screenshot on failure in TestNG?

Implement ITestListener.onTestFailure(), get the current thread's WebDriver, call getScreenshotAs() and attach the file to your report; register the listener in testng.xml or with @Listeners.

How do you apply a RetryAnalyzer to all tests?

Implement IAnnotationTransformer and call annotation.setRetryAnalyzer(RetryAnalyzer.class) in transform(), then register that transformer as a listener in testng.xml.

What is the difference between ITestListener and ISuiteListener?

ITestListener reacts to individual tests and each <test> context; ISuiteListener runs once when a whole suite starts and finishes.