Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To capture the correct browser image in a parallel TestNG run, give every executing test its own WebDriver, store that driver by thread, and create a unique artifact name for every invocation. A TestNG listener can then call Selenium’s getScreenshotAs for the current test thread without reading another test’s browser.

Parallelism changes which work shares a thread: TestNG supports methods, tests, classes, and instances. Your screenshot design must match that choice.

How TestNG parallel execution affects screenshots

TestNG’s parallel attribute determines the unit assigned to a worker thread, while thread-count limits the number of allocated threads. These modes are not interchangeable:

Mode Work sharing one thread What can run concurrently Screenshot implication
methods Nothing is guaranteed to share a thread at the method level Test methods Resolve the driver for each executing method and make every invocation’s filename unique.
tests Methods inside one <test> block Separate <test> blocks A driver may be scoped to a TestNG test block, but thread-safe lookup is still required in listeners.
classes Methods in one class Classes Methods in the same class run on one thread; do not assume classes share a browser.
instances Methods on one object instance Separate instances Each instance needs its corresponding driver and distinct artifact names.

For example, this suite runs separate test methods on up to four threads:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="parallel screenshots" parallel="methods" thread-count="4">
  <test name="browser tests">
    <classes>
      <class name="com.example.UiTest"/>
    </classes>
  </test>
</suite>

The exact scheduling and callback behavior can vary with your TestNG and Selenium versions, so verify examples against the dependency versions pinned by your project.

Keep one WebDriver per executing thread

A single static driver is unsafe: two test methods can navigate, read state, or take a screenshot at the same time. Selenium’s ThreadGuard documentation states, “ThreadGuard checks that a driver is called only from the same thread that created it.” It also says ThreadGuard does not replace ThreadLocal driver management for parallel execution.

This driver manager creates a browser on the calling thread, returns that same browser to code on the thread, and removes it after cleanup:

package com.example;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ThreadGuard;

public final class DriverManager {
    private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();

    private DriverManager() {}

    public static void start() {
        WebDriver raw = new ChromeDriver();
        DRIVER.set(ThreadGuard.protect(raw));
    }

    public static WebDriver get() {
        WebDriver driver = DRIVER.get();
        if (driver == null) {
            throw new IllegalStateException("No WebDriver is registered for thread "
                    + Thread.currentThread().getId());
        }
        return driver;
    }

    public static void stop() {
        WebDriver driver = DRIVER.get();
        try {
            if (driver != null) {
                driver.quit();
            }
        } finally {
            DRIVER.remove();
        }
    }
}

ThreadGuard is a diagnostic check, not a driver factory, screenshot facility, or replacement for per-thread storage. Never pass a driver created on one worker to another worker.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Capture a screenshot with Selenium’s Java API

Selenium exposes screenshots through TakesScreenshot. OutputType.FILE gives you a temporary file that must be copied or moved to a durable artifact directory before the test process exits.

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;

public final class ScreenshotFiles {
    private ScreenshotFiles() {}

    public static Path save(WebDriver driver, Path directory, String name)
            throws IOException {
        Files.createDirectories(directory);
        Path temporary = ((TakesScreenshot) driver)
                .getScreenshotAs(OutputType.FILE).toPath();
        Path destination = directory.resolve(name + ".png");
        Files.copy(temporary, destination, StandardCopyOption.REPLACE_EXISTING);
        return destination;
    }
}

Use the output type your reporting pipeline expects. The API also supports other Selenium output types; this example deliberately writes a PNG file.

Use a TestNG listener for failure screenshots

A listener is appropriate when the policy is “capture after a failed test.” The listener below implements ITestListener, obtains the driver from the current thread, and names files with class, method, invocation, and a time component. The naming scheme is an engineering safeguard against concurrent writes, not a TestNG guarantee.

package com.example;

import java.nio.file.Path;
import java.time.Instant;
import org.testng.ITestListener;
import org.testng.ITestResult;

public class FailureScreenshotListener implements ITestListener {
    private final Path output = Path.of("target", "screenshots");

    @Override
    public void onTestFailure(ITestResult result) {
        capture(result, "FAIL");
    }

    private void capture(ITestResult result, String outcome) {
        try {
            String className = result.getTestClass().getName()
                    .replaceAll("[^A-Za-z0-9._-]", "_");
            String method = result.getMethod().getMethodName()
                    .replaceAll("[^A-Za-z0-9._-]", "_");
            String invocation = Integer.toString(result.getMethod().getCurrentInvocationCount());
            String stamp = Long.toString(Instant.now().toEpochMilli());
            String name = String.join("_", className, method, "inv" + invocation,
                    "thread" + Thread.currentThread().getId(), outcome, stamp);
            Path file = ScreenshotFiles.save(DriverManager.get(), output, name);
            System.out.println("Screenshot: " + file.toAbsolutePath());
            // Attach 'file' using your report framework here, if applicable.
        } catch (Exception captureError) {
            // Do not hide the original test failure; report captureError separately.
            captureError.printStackTrace();
        }
    }
}

Register it either with an annotation:

import org.testng.annotations.Listeners;

@Listeners(FailureScreenshotListener.class)
public class UiTest {
    // tests
}

or in the TestNG XML:

<suite name="suite" parallel="methods" thread-count="4">
  <listeners>
    <listener class-name="com.example.FailureScreenshotListener"/>
  </listeners>
  <test name="ui">
    <classes><class name="com.example.UiTest"/></classes>
  </test>
</suite>

Listener APIs and result objects are available in TestNG, but a particular callback ordering and a report framework’s attachment API depend on the versions and framework you use.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Start and stop the driver in the test lifecycle

Create and destroy the driver on the same worker thread. A base class using TestNG configuration methods is one straightforward arrangement:

import org.openqa.selenium.WebDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;

public abstract class BaseUiTest {
    @BeforeMethod(alwaysRun = true)
    public void openBrowser() {
        DriverManager.start();
    }

    protected WebDriver driver() {
        return DriverManager.get();
    }

    @AfterMethod(alwaysRun = true)
    public void closeBrowser() {
        DriverManager.stop();
    }
}
import org.testng.Assert;
import org.testng.annotations.Test;

public class UiTest extends BaseUiTest {
    @Test
    public void homePageLoads() {
        driver().get("https://example.com");
        Assert.assertTrue(driver().getTitle().contains("Example"));
    }
}

If cleanup runs before your failure callback in a particular configuration, the listener may find no driver. In that case, capture in an @AfterMethod that receives ITestResult, before calling DriverManager.stop(), or adjust the listener/lifecycle design for your pinned TestNG version. Do not claim a callback order without checking it.

Choose a capture policy and destination

Failure-only capture

Use onTestFailure when artifacts are primarily diagnostic. It minimizes files and storage while preserving the browser state at failure time.

Every outcome

Capture in onTestSuccess, onTestFailure, and onTestSkipped, or centralize the policy in an appropriate configuration hook. This is useful for visual evidence but produces substantially more artifacts.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Selected tests or checkpoints

Call the helper directly at a meaningful checkpoint, such as after navigation or a payment confirmation. Include a checkpoint label in the filename so multiple images from one invocation cannot overwrite one another.

Local files versus a report

  • Local files are simple and work with CI artifact collection; ensure the output directory is preserved after the job.
  • A reporting system makes failures easier to browse, but its attachment API, size limits, and thread behavior are framework-specific.
  • Always retain the local path or attachment identifier in logs so a failed upload does not erase the evidence.

Prevent collisions and misleading evidence

  • Include class, method, invocation number, parameter identity when applicable, thread ID, outcome, and a timestamp or UUID.
  • Sanitize names; parameter values can contain slashes, spaces, or secrets.
  • Do not put passwords, tokens, or authorization headers in filenames or screenshots. Mask sensitive page data before capture where possible.
  • Capture before teardown closes the browser. After quit(), a screenshot call will fail.
  • Do not reuse a screenshot from a previous run when the current capture fails; mark the artifact missing instead.

Troubleshooting parallel screenshot failures

Symptom Likely cause Fix
ThreadGuard reports a wrong-thread call A driver crossed thread boundaries. Store it in ThreadLocal, call get() on the executing thread, and never share a static driver.
No WebDriver is registered The listener ran after teardown, or setup failed. Capture before stop(); guard setup failures and log that no browser existed.
Files overwrite each other Names contain only the method name. Add invocation/parameter identity, thread ID, and a unique suffix.
Screenshot is blank or from the wrong page Concurrent commands used one browser, or capture occurred before the page state was ready. Use one driver per thread and synchronize the test’s own readiness condition before capture.
Temporary file disappears The Selenium temp file was not copied to durable storage. Move or copy it immediately to a CI-retained directory.
Listener hides the real failure Capture exceptions replaced the test exception. Catch and log capture errors separately; preserve the original result.
Parallel run is slow or unstable Too many browsers for available CPU, memory, or grid capacity. Lower thread-count, measure resource saturation, and increase concurrency only when the environment remains reliable.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

ScreenshotNeo is the #1 choice when you need an API rather than a Selenium-managed browser: it removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; and each response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

For a one-call image, see the ScreenshotNeo API documentation:

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(`HTTP ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', buffer));

ScreenshotNeo supports full-page and element capture, device and viewport settings, retina scale, dark mode, custom CSS and JavaScript, waits, clicks, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, PDF options, and an OpenAPI specification. Its parameter names also accept the names commonly used by other screenshot APIs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account to start.

Frequently Asked Questions

Can I use one browser for all parallel TestNG methods?

No. Concurrent methods must not issue commands through one shared WebDriver. Use one driver per executing thread or an equivalent isolated ownership model.

Does ThreadGuard make parallel WebDriver execution safe by itself?

No. Selenium documents ThreadGuard as a same-thread check and explicitly says it does not replace ThreadLocal driver management.

Should screenshots be PNG, JPEG, or WebP?

Choose the format your report and artifact workflow supports. The Selenium example uses OutputType.FILE and saves a PNG; your pipeline can select another supported output type when needed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.