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 Interface | Key Methods | Common Use |
|---|---|---|
| ITestListener | onTestStart, onTestSuccess, onTestFailure, onTestSkipped | Screenshot on fail, ExtentReports logging, Slack notification |
| ISuiteListener | onStart(ISuite), onFinish(ISuite) | Suite-level reporting, DB logging |
| ITestResult (not a listener) | getStatus, getName, getThrowable, getStartMillis | Access test result details inside listeners |
| IReporter | generateReport(List<XmlSuite>, List<ISuite>, String outputDirectory) | Generate custom HTML/XML/JSON reports |
| IRetryAnalyzer | retry(ITestResult) — return true to retry | Automatically retry flaky tests |
| IAnnotationTransformer | transform(ITestAnnotation, ...) — modify annotations | Programmatically add retryAnalyzer to all tests |
| IInvokedMethodListener | beforeInvocation, afterInvocation | Intercept every method call (before/after each test/config) |
| IAlterSuiteListener | alter(List<XmlSuite>) | Modify test suite programmatically at runtime |
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
Method 1 — testng.xml (Recommended for Suite-Level)
<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"/>
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.