ScreenshotNeo

BlogHow-to

How to Use TestNG Listeners in Selenium WebDriver

Connect TestNG lifecycle callbacks to Selenium tests, capture failure screenshots before teardown, and choose the right listener and registration method.

By the ScreenshotNeo team4 October 20268 min read

Use a TestNG listener to run code when a test or suite reaches a lifecycle event. For Selenium failure screenshots, implement ITestListener, retrieve the WebDriver belonging to the failed test, and save the screenshot to a durable artifact path inside onTestFailure—before teardown quits that driver. Register an ordinary listener in testng.xml or with @Listeners.

Choose the listener for the event

TestNG provides several listener interfaces that let you respond to or change its behavior. Pick one based on the lifecycle event and when your output is needed.

Need Interface Use it for
React to individual test starts, passes, failures, and skips during execution ITestListener Live logging, notifications, and failure screenshots
Observe suite start and finish ISuiteListener Suite-level setup or cleanup
Observe class processing boundaries IClassListener Actions before or after a test class is processed
Observe setup or teardown configuration outcomes IConfigurationListener Results for configuration methods
Build an aggregate report after suites finish IReporter Post-run report generation
Change annotations before TestNG processes tests IAnnotationTransformer Early annotation changes

ITestListener is the usual starting point when you need a reaction to each test method result as it occurs. Use IReporter when you need the completed run’s results to assemble a report after execution.

Register an ordinary listener

Option 1: Register it in testng.xml

For suite-wide behavior, XML makes the registration visible beside the suite definition. Use the listener’s fully qualified class name.

<suite name="UI suite">
  <listeners>
    <listener class-name="com.example.ScreenshotListener" />
  </listeners>
  <test name="Browser tests">
    <classes>
      <class name="com.example.CheckoutTest" />
    </classes>
  </test>
</suite>

Run the suite using the TestNG runner configured for your project, such as your build tool or IDE. The XML file must be the one passed to that runner, and the listener class must be on the test runtime classpath.

Option 2: Use @Listeners

Annotate a test class when annotation-based registration fits your suite:

import org.testng.annotations.Listeners;

@Listeners(com.example.ScreenshotListener.class)
public class CheckoutTest {
  // Test methods
}

TestNG documents that @Listeners applies to the entire suite file as though configured in testng.xml. If you need class-specific exclusions, add filtering logic to the listener or use a registration arrangement whose scope matches the requirement.

Other supported registration paths

TestNG also supports registration through its API and discovery through Java ServiceLoader. Programmatic registration can fit a runner that constructs TestNG itself. ServiceLoader discovery makes classpath contents part of the suite’s behavior, so document shared listeners and dependencies for maintainers.

Important exception: IAnnotationTransformer

Do not register IAnnotationTransformer with @Listeners. TestNG must know about it before parsing annotations and says it will ignore this registration route for that interface. Register it through suite XML or another supported early registration path.

Capture a Selenium screenshot on failure

Selenium’s Java API exposes screenshots through TakesScreenshot. The returned file can be temporary, so copy it to a durable location before the browser is closed. The example below is a complete listener class apart from DriverStore.current(), which is deliberately a project-specific integration point: TestNG does not define how your tests store their WebDriver.

package com.example;

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

public class ScreenshotListener implements ITestListener {
  @Override
  public void onTestFailure(ITestResult result) {
    WebDriver driver = DriverStore.current(); // Replace with your test framework's lookup.
    if (!(driver instanceof TakesScreenshot)) {
      System.err.println("No screenshot-capable WebDriver for " + result.getName());
      return;
    }

    File temporary = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
    String safeName = result.getName().replaceAll("[^A-Za-z0-9._-]", "_");
    Path destination = Path.of("target", "screenshots", safeName + "-"
        + System.nanoTime() + ".png");
    try {
      Files.createDirectories(destination.getParent());
      Files.copy(temporary.toPath(), destination, StandardCopyOption.REPLACE_EXISTING);
      System.out.println("Failure screenshot: " + destination.toAbsolutePath());
    } catch (IOException e) {
      System.err.println("Could not save screenshot for " + result.getName() + ": " + e);
    }
  }
}

This example uses Path.of, available in Java 11 and later. On older Java versions, construct the path with Paths.get("target", "screenshots", filename). Add a DriverStore implementation that returns the driver for the current test.

Keep driver lookup safe in parallel runs

Do not use one mutable global driver for a parallel suite. Associate each test or worker thread with its own driver, and make the listener retrieve the driver for the failing test. A thread-local store is one possible design when test creation, callbacks, and cleanup all use the same worker thread; otherwise use a test-context keyed store. Clear the association during cleanup to avoid stale references. These are integration choices, because TestNG does not prescribe WebDriver storage.

Capture and persist the file before teardown calls quit(). If the driver is already closed, the callback cannot use it to take a browser screenshot. Keep screenshot naming unique: data-driven invocations can share a method name, and parallel failures can overwrite a path based only on that name.

Other screenshot output forms

Selenium’s screenshot API also supports byte-array and Base64 output forms. Use bytes if your artifact system accepts an in-memory payload; use Base64 when the receiving interface specifically expects encoded image data. For a local file artifact, the file output form and an explicit copy make the save step clear.

Decide between a listener and a reporter

A listener receives events during execution, so it suits immediate actions such as logging each failure or saving its screenshot. A reporter runs after suites have run, so it suits reports assembled from the completed run. You can use both when you need per-event artifacts and a final summary, but keep their responsibilities distinct: save browser-dependent evidence while the driver is available, then aggregate results after execution.

Make failure artifacts useful

  1. Choose a stable output directory. Store artifacts somewhere your build or CI system can collect, and create the directory before copying.
  2. Make filenames safe and unique. Include a sanitized test name and an invocation or unique suffix; consider class name and parameters when those distinguish cases.
  3. Preserve failure context. Log the test name and saved path. If your reporting system supports attachments, connect that path to the corresponding result.
  4. Handle screenshot errors separately. A screenshot failure should be logged without hiding the original test failure.
  5. Check artifact retention. Screenshots can contain sensitive page data. Apply your team’s access and retention rules to the output directory.

Performance, reliability, and cost considerations

  • Capture selectively. Taking and copying an image adds work and storage. Capturing only failures usually avoids that overhead on passing tests.
  • Keep callbacks small. A synchronous callback can delay suite progress while it writes files. Avoid unrelated network calls or expensive report generation in every test callback.
  • Expect environment limits. A remote WebDriver, headless browser, or failed browser session may not be able to produce a screenshot. Log the artifact failure while preserving the original result.
  • Plan storage. Full test suites can create many images. Use unique paths, CI artifact collection, and retention limits suited to your debugging needs.
  • There is no universal timing or cost figure. Capture time and storage depend on browser, page, execution environment, and artifact handling; measure them in the environment where the suite runs.

Troubleshooting

Symptom Likely cause Fix
Listener callbacks never run The suite runner did not load the XML file, the listener class is absent from the test runtime classpath, or registration is out of scope. Confirm the runner uses the expected suite file, verify the fully qualified class name and classpath, and check the registration scope.
IAnnotationTransformer is ignored It was registered with @Listeners. Register it early through suite XML or another supported early registration mechanism.
Screenshot call fails after a test failure The driver is null, already quit, or not screenshot-capable. Look up the driver associated with the failing test, capture before teardown, and check instanceof TakesScreenshot.
Screenshot is missing after the run The Selenium temporary file was not copied, the output directory was not created, or the artifact collector ignores the path. Copy to a durable path, create parent directories, and configure the runner or CI system to collect that directory.
One failure overwrites another image Filenames use only a shared test method name. Add class, invocation, parameter, or unique run information to the sanitized filename.
Wrong browser is captured during parallel execution Tests share mutable driver state or the lookup returns a different worker’s driver. Isolate driver state per test or worker and verify the callback uses the failing test’s association.
Final report lacks late failure details Report generation happens before execution is complete or relies on a live callback for aggregation. Use IReporter for post-suite aggregation and keep immediate artifact capture in the listener.

Or skip the browser setup

If the goal is a website screenshot rather than browser automation, ScreenshotNeo provides a screenshot API and MCP server for developers. Its one-call API can return an image or PDF; see the ScreenshotNeo API documentation for options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res); // Bun example for saving the response body

The supplied Node.js request pattern uses standard fetch; the final line shows one way to save its response with Bun. In Node.js, use fs/promises to save the response body instead:

import { writeFile } from 'node:fs/promises';

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
  • Cookie banners, newsletter popups, and chat widgets are removed before the screenshot; each cleanup step can be turned off.
  • Bot checks or CAPTCHAs, blank pages, failed loads, timeouts, and cache hits are not billed. Response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
  • The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Can one listener implement more than one TestNG interface?

Yes. Implement the interfaces that match the events you need, keeping callbacks focused and avoiding duplicate work.

Should every passing test get a screenshot?

Usually only if visual evidence is part of the test’s purpose. For failure diagnosis, capture on failure to limit extra files and callback work.

Does TestNG know which WebDriver belongs to a test?

No. Your test framework must own driver creation, lookup, isolation, and cleanup; the listener uses that integration.