---
title: Listeners, screenshots and reports | TestingBot
description: A failing UI test with only a stack trace leaves you guessing what the
  page looked like. TestNG listeners let you run code on every test event without…
source_url:
  html: https://testingbot.com/resources/courses/selenium-testng/listeners-and-reports
  md: https://testingbot.com/resources/courses/selenium-testng/listeners-and-reports.md
---

Course

[Selenium Java Tutorial with TestNG](https://testingbot.com/resources/courses/selenium-testng)

Chapter 9 of 11

1. [1 Selenium and TestNG: how they fit together](https://testingbot.com/resources/courses/selenium-testng)
2. [2 Set up a Maven project for Selenium](https://testingbot.com/resources/courses/selenium-testng/project-setup)
3. [3 Your first Selenium test with TestNG](https://testingbot.com/resources/courses/selenium-testng/first-test)
4. [4 TestNG annotations and the driver lifecycle](https://testingbot.com/resources/courses/selenium-testng/annotations)
5. [5 Hard assertions vs SoftAssert](https://testingbot.com/resources/courses/selenium-testng/assertions)
6. [6 Data-driven tests with DataProvider](https://testingbot.com/resources/courses/selenium-testng/data-providers)
7. [7 Suites, groups and parameters in testng.xml](https://testingbot.com/resources/courses/selenium-testng/testng-xml)
8. [8 Run Selenium tests in parallel](https://testingbot.com/resources/courses/selenium-testng/parallel-execution)
9. [9 Listeners, screenshots and reports](https://testingbot.com/resources/courses/selenium-testng/listeners-and-reports)
10. [10 Run the suite on a Selenium grid](https://testingbot.com/resources/courses/selenium-testng/run-on-a-selenium-grid)
11. [11 Knowledge check](https://testingbot.com/resources/courses/selenium-testng/knowledge-check)

[Exit course](https://testingbot.com/resources/courses)

 Chapter 9 of 11 Intermediate 7 min read 3 quick checks Updated Nov 2026 

# Listeners, screenshots and reports
On this page
1. [ITestListener in one minute](https://testingbot.com#itestlistener-in-one-minute)
2. [Screenshot on failure](https://testingbot.com#screenshot-on-failure)
3. [Registering a listener](https://testingbot.com#registering-a-listener)
4. [The reports TestNG already writes](https://testingbot.com#the-reports-testng-already-writes)
5. [Retrying flaky tests, carefully](https://testingbot.com#retrying-flaky-tests-carefully)
6. [Logging what the test did](https://testingbot.com#logging-what-the-test-did)

A failing UI test with only a stack trace leaves you guessing what the page looked like. TestNG listeners let you run code on every test event without touching the tests themselves, which makes them the right place for screenshots on failure, extra logging and custom reporting.

## ITestListener in one minute[#](https://testingbot.com#itestlistener-in-one-minute)

`org.testng.ITestListener` has a callback for each stage of a test's life. All methods have default empty implementations, so you override only the ones you need:

| Method | Called |
| --- | --- |
| `onStart(ITestContext)` | before any test in a `<test>` block runs |
| `onTestStart(ITestResult)` | before each test method invocation |
| `onTestSuccess(ITestResult)` | after a passing test |
| `onTestFailure(ITestResult)` | after a failing test |
| `onTestSkipped(ITestResult)` | after a skipped test (including a failed dependency or a retried attempt) |
| `onFinish(ITestContext)` | after all tests in the `<test>` block |

`ITestResult` carries what you need to know about the test: `getName()`, `getMethod()`, `getTestClass()`, `getParameters()` (the data provider row), `getThrowable()` (the failure), `getStartMillis()` and `getEndMillis()`.

## Screenshot on failure[#](https://testingbot.com#screenshot-on-failure)

With the `ThreadLocal` base class from chapter 7, a listener can reach the current thread's driver through `BaseTest.driver()`:

```java
package com.example;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.testng.ITestListener;
import org.testng.ITestResult;
import org.testng.Reporter;

public class ScreenshotListener implements ITestListener {

    @Override
    public void onTestFailure(ITestResult result) {
        WebDriver driver = BaseTest.driver();
        if (driver == null) {
            return; // failed before a browser existed
        }
        byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
        Path file = Path.of("target", "screenshots",
                result.getTestClass().getRealClass().getSimpleName() + "-"
                        + result.getMethod().getMethodName() + "-"
                        + result.getStartMillis() + ".png");
        try {
            Files.createDirectories(file.getParent());
            Files.write(file, png);
            Reporter.log("Screenshot: " + file.toAbsolutePath(), true);
        } catch (IOException e) {
            Reporter.log("Could not save screenshot: " + e.getMessage(), true);
        }
    }
}
```

Points worth noticing:

- **Timing.** This only works if the browser is still open when the listener runs. We checked the order with TestNG 7.12.0: for a failing test, `onTestFailure` runs _before_ the `@AfterMethod` that quits the driver, so the screenshot shows the page as it was at the moment of failure.
- **Same thread.** The listener callback runs on the test's thread, so `BaseTest.driver()` returns the right browser even under `parallel="methods"`.
- **Unique file names.** Data-driven tests have the same method name for every row, and in parallel runs several can fail at once. Including the start time keeps one row's screenshot from overwriting another's. Our first version used just class and method name and lost three of four screenshots exactly that way.
- **`OutputType.BYTES`** avoids a temporary file and an extra dependency for copying it. `OutputType.FILE` plus a copy works too.
- **`Reporter.log(message, true)`** writes the message into TestNG's HTML reports for that test and, with `true`, to the console as well.

A listener instance is shared by all threads, so don't keep per-test state in its fields. If you need to, use a `ConcurrentHashMap` keyed by `ITestResult` or a `ThreadLocal`.

Your listener stores the current test's name in a field in `onTestStart` and reads it in `onTestFailure`. Under `parallel="methods"`, failures get the wrong name. Why?

 One listener instance is shared by all threads, so the field is overwritten by other tests `onTestStart` runs after `onTestFailure` Listeners do not support parallel runs

Keep per-test state out of listener fields, or key it by `ITestResult` in a `ConcurrentHashMap` or use a `ThreadLocal`.

## Registering a listener[#](https://testingbot.com#registering-a-listener)

There are three ways, from most local to most global:

**1. On a class** , for that class (and in practice, the whole suite: TestNG registers listeners declared with `@Listeners` globally):

```java
import org.testng.annotations.Listeners;

@Listeners(ScreenshotListener.class)
public class FirstTest extends BaseTest { ... }
```

**2. In `testng.xml`** , which keeps tests free of reporting concerns and is what we recommend:

```xml
<suite name="Course suite" parallel="methods" thread-count="3">
  <listeners>
    <listener class-name="com.example.ScreenshotListener"/>
  </listeners>
  ...
</suite>
```

**3. Via Java's ServiceLoader** : list the class name in `src/test/resources/META-INF/services/org.testng.ITestNGListener`. TestNG then picks it up for every run, including `mvn test -Dtest=...`, which ignores the suite file. Useful for a company-wide listener packaged in a shared library.

A company-wide listener must run for every project, including runs started with `mvn test -Dtest=...`. How do you register it?

 With `@Listeners` on one test class In each project's `testng.xml` In `@BeforeSuite` Through Java's ServiceLoader, in `META-INF/services/org.testng.ITestNGListener`

ServiceLoader registration is picked up for every run, even when the suite file is bypassed.

## The reports TestNG already writes[#](https://testingbot.com#the-reports-testng-already-writes)

Run `mvn test` and look in `target/surefire-reports`:

```
index.html TestNG's interactive report: suites, groups, times, output
emailable-report.html a single self-contained summary page
testng-results.xml TestNG's machine-readable results
junitreports/ JUnit-style XML per class
TEST-TestSuite.xml Surefire's JUnit-style XML for the whole run
```

CI servers (GitHub Actions test reporters, Jenkins, GitLab) understand the JUnit-style XML, so point them at `target/surefire-reports/*.xml` and archive `target/screenshots` as a build artifact. Third-party reporters such as Allure and ExtentReports plug in the same way as our screenshot listener: as a TestNG listener added to the suite.

## Retrying flaky tests, carefully[#](https://testingbot.com#retrying-flaky-tests-carefully)

TestNG can re-run a failed test through an `IRetryAnalyzer`:

```java
package com.example;

import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;

public class RetryOnce implements IRetryAnalyzer {
    private int attempts = 0;

    @Override
    public boolean retry(ITestResult result) {
        return attempts++ < 1; // retry a failed test one time
    }
}
```

```java
@Test(retryAnalyzer = RetryOnce.class)
public void submitsTheWebForm() { ... }
```

Listeners see the failed first attempt as a skip (`onTestSkipped`), TestNG's summary counts it under "Retries", and the final attempt is reported as passed or failed. When we ran two failing methods and a two-row data provider with this analyzer on TestNG 7.12.0, every method and every row was retried exactly once, so the counter can safely live in a field.

Use retries as a short-term bandage with a ticket attached, not as a strategy. A test that passes on the second attempt is telling you about a race condition, an environment problem or a real intermittent bug, and retries hide all three. Watch your reports for tests that are frequently retried, and fix the wait or the test data instead. Our article on [exception handling with Selenium](https://testingbot.com/resources/articles/exception-handling-with-selenium) covers the exceptions that most often sit behind flaky failures.

With a retry analyzer, a test fails once and passes on the second attempt. Which listener callback receives the first attempt?

 `onTestFailure` `onTestSuccess` `onTestSkipped`

The retried attempt is reported as a skip and counted under Retries. If your screenshot listener only handles failures, it will not capture the first attempt.

## Logging what the test did[#](https://testingbot.com#logging-what-the-test-did)

Two cheap additions make failures much easier to read:

```java
@Override
public void onTestStart(ITestResult result) {
    Reporter.log("START " + result.getMethod().getQualifiedName()
            + " params=" + java.util.Arrays.toString(result.getParameters()), true);
}
```

and, in the failure handler, the current URL and page title (`driver.getCurrentUrl()`, `driver.getTitle()`), which often reveal an unexpected redirect to a login or error page before you even open the screenshot.

Local screenshots cover the moment of failure. When tests run on a remote grid, you usually also get a video and logs of the whole session; the next chapter moves the suite there, and the [TestingBot test options](https://testingbot.com/support/web-automate/selenium/test-options) list how to control recording.

[Previous · Chapter 8 Run Selenium tests in parallel](https://testingbot.com/resources/courses/selenium-testng/parallel-execution) [Next · Chapter 10 Run the suite on a Selenium grid](https://testingbot.com/resources/courses/selenium-testng/run-on-a-selenium-grid)

On this page

1. [ITestListener in one minute](https://testingbot.com#itestlistener-in-one-minute)
2. [Screenshot on failure](https://testingbot.com#screenshot-on-failure)
3. [Registering a listener](https://testingbot.com#registering-a-listener)
4. [The reports TestNG already writes](https://testingbot.com#the-reports-testng-already-writes)
5. [Retrying flaky tests, carefully](https://testingbot.com#retrying-flaky-tests-carefully)
6. [Logging what the test did](https://testingbot.com#logging-what-the-test-did)

Certification

This course has a free, hands-on certificate.

[How it works](https://testingbot.com/resources/courses/selenium-testng/certification)
