ScreenshotNeo

BlogHow-to

How to Run Selenium Tests in Parallel with TestNG

Configure TestNG parallel execution safely with isolated WebDriver sessions, Selenium Grid, tuning guidance, troubleshooting, and runnable Java examples.

By the ScreenshotNeo team1 October 202610 min read

TestNG runs Selenium tests in parallel when you set the suite’s parallel mode and thread-count, then give every concurrently running test its own browser session and independent test data. Start with a modest thread count, verify stability and resource usage, and increase it gradually.

The smallest working configuration is:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Parallel Suite" parallel="methods" thread-count="4">
  <test name="UI tests">
    <classes>
      <class name="tests.LoginTest"/>
      <class name="tests.CheckoutTest"/>
    </classes>
  </test>
</suite>

parallel="methods" allows eligible test methods to run on separate threads. thread-count="4" is the maximum number of TestNG worker threads allocated by this suite. TestNG also supports classes, tests, and instances; the correct choice depends on what your tests share. See the TestNG documentation for version-specific details.

1. Choose the TestNG parallel mode

Mode What stays grouped Use it when Risk to check
methods Methods can execute concurrently Methods are independent and you need the most method-level parallelism Class fields, drivers, and test data must be isolated
classes Methods in one class run on the same thread Classes are independent but methods in a class share setup Parallelism is limited by the number of classes
tests Methods in each XML <test> group run together XML groups represent browser, environment, or feature boundaries Groups must not collide on shared state
instances Methods on one object instance share a thread Each instance represents an independent test context Instances must not share mutable resources

Use the narrowest mode that preserves your test assumptions. If methods use mutable class fields or a shared fixture, begin with classes or tests, or refactor the shared state before switching to methods.

2. Create one WebDriver per concurrent test

A WebDriver session is mutable: navigation, cookies, windows, and the current page all change during a test. Do not let concurrent methods reuse one static driver. A common Java design stores a driver in ThreadLocal, creates it in a TestNG lifecycle hook, and always calls quit() in teardown.

package tests;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

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

    @BeforeMethod
    public void setUp() {
        DRIVER.set(new ChromeDriver());
    }

    private WebDriver driver() {
        WebDriver driver = DRIVER.get();
        if (driver == null) {
            throw new IllegalStateException("No WebDriver is associated with this test thread");
        }
        return driver;
    }

    @Test
    public void validLogin() {
        driver().get("https://example.test/login");
        driver().findElement(By.id("email")).sendKeys("user@example.test");
        driver().findElement(By.id("password")).sendKeys("test-password");
        driver().findElement(By.cssSelector("button[type='submit']")).click();
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown() {
        WebDriver current = DRIVER.get();
        try {
            if (current != null) {
                current.quit();
            }
        } finally {
            DRIVER.remove();
        }
    }
}

Selenium Manager can discover and manage browser drivers in current Selenium releases. If your project pins drivers another way, keep that setup outside the test method. The important properties are one session per concurrent execution context and cleanup even after failures.

Keep test data independent

  • Generate a unique email, order ID, or record key for each invocation.
  • Do not have two tests update the same account or delete each other’s data.
  • Use separate download directories and unique filenames.
  • Avoid mutable static collections, singleton page objects, and shared cookies.
  • Make cleanup idempotent so a failed test does not poison the next one.

3. A complete Maven example

This project uses Selenium Java and TestNG. Pin versions appropriate for your project; the lifecycle and suite configuration are the parts that control parallel execution.

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>parallel-ui-tests</artifactId>
  <version>1.0-SNAPSHOT</version>
  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <selenium.version>4.25.0</selenium.version>
    <testng.version>7.10.2</testng.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>${selenium.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>${testng.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.0</version>
        <configuration>
          <suiteXmlFiles>
            <suiteXmlFile>testng.xml</suiteXmlFile>
          </suiteXmlFiles>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Run it with mvn test. Put the XML file at the project root or update the Surefire path. Do not also configure a conflicting parallel mode in Surefire until you understand which layer is scheduling your tests.

4. Control browser parameters with XML groups

Use separate XML <test> blocks when each group needs a distinct browser or environment parameter. With parallel="tests", each block can run on its own worker.

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Browser matrix" parallel="tests" thread-count="3">
  <test name="Chrome">
    <parameter name="browser" value="chrome"/>
    <classes><class name="tests.SmokeTest"/></classes>
  </test>
  <test name="Firefox">
    <parameter name="browser" value="firefox"/>
    <classes><class name="tests.SmokeTest"/></classes>
  </test>
</suite>
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Parameters;

public class SmokeTest {
    private WebDriver driver;

    @BeforeMethod
    @Parameters("browser")
    public void setUp(String browser) {
        driver = switch (browser.toLowerCase()) {
            case "chrome" -> new ChromeDriver();
            case "firefox" -> new FirefoxDriver();
            default -> throw new IllegalArgumentException("Unsupported browser: " + browser);
        };
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown() {
        if (driver != null) {
            driver.quit();
            driver = null;
        }
    }
}

5. Run tests on Selenium Grid

Selenium Grid runs test suites in parallel against multiple machines called Nodes. Grid is useful when one machine cannot provide enough browser sessions or when you need browser, version, and operating-system coverage.

For a local evaluation, download Selenium Server and start standalone mode:

java -jar selenium-server-4.25.0.jar standalone

Standalone puts Grid components in one process on one machine. It is not a distributed multi-machine deployment. A Java test can connect to the documented local endpoint:

import java.net.MalformedURLException;
import java.net.URI;
import org.openqa.selenium.MutableCapabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.RemoteWebDriver;

public final class GridDriverFactory {
    private GridDriverFactory() {}

    public static WebDriver create() throws MalformedURLException {
        MutableCapabilities capabilities = new MutableCapabilities();
        capabilities.setCapability("browserName", "chrome");
        return new RemoteWebDriver(
            URI.create("http://localhost:4444").toURL(),
            capabilities
        );
    }
}

For larger deployments, choose standalone, Hub/Node, or distributed roles based on the number of machines, browser matrix, and required concurrent sessions. Protect Grid with firewall controls: an exposed Grid can provide access to infrastructure and internal applications or allow third parties to run binaries. See Selenium’s Grid getting-started guide.

Plan capacity from measurements

  • CPU and RAM limit how many browser processes a machine can run reliably.
  • Grid slots, browser-specific limits, application response time, and network latency affect throughput.
  • Selenium’s guide uses approximately 1 GB RAM per browser as a planning reference; treat it as guidance, not a guarantee.
  • The guide’s four-CPU Distributor and eight-CPU Node examples illustrate default capacity behavior; they are not universal limits.
  • Small nodes can isolate failures better than one very large node.

6. Tune thread count without destabilizing the suite

  1. Run serially and record elapsed time and failure rate.
  2. Set a modest thread-count that fits available browser sessions.
  3. Inspect CPU, memory, browser startup time, Grid queue time, and application response time.
  4. Increase the count one step at a time and compare completed tests per minute and retry or timeout rates.
  5. Stop increasing when throughput stops improving or isolation failures appear.

A simple estimate such as test count × average duration ÷ node count ignores setup, scheduling, dependencies, queueing, and resource overhead. Selenium’s documentation examples (15 tests at 45 seconds across one, five, or 15 nodes; 100 tests at 120 seconds across 15 nodes) are illustrations, not promises.

7. Data providers and additional TestNG pools

Data-driven tests can create another source of concurrency. TestNG documents a data-provider thread pool and additional controls beginning with TestNG 7.9.0. Defaults and available attributes vary by TestNG version, so check the version used by your build before relying on a default. Keep the data-provider pool, suite thread-count, Grid slots, and machine capacity aligned.

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class SearchTest {
    @DataProvider(name = "queries", parallel = true)
    public Object[][] queries() {
        return new Object[][] {
            { "selenium" },
            { "testng" },
            { "webdriver" }
        };
    }

    @Test(dataProvider = "queries")
    public void search(String query) {
        // Create a separate driver and isolated data for each invocation.
    }
}

8. Troubleshooting parallel TestNG runs

Symptom Likely cause Fix
Tests pass alone but fail in parallel Shared driver, class field, account, record, or file Give each execution its own driver and unique data; remove mutable static state
“Session not created” or browser startup failures More threads than available browser capacity, incompatible browser/driver, or exhausted Grid slots Lower thread-count, inspect Grid slots and machine resources, and align browser versions
WebDriver is null in a test Setup did not run, the wrong lifecycle annotation was used, or a thread-local value was never set Use @BeforeMethod or the intended hook consistently and fail clearly when no driver exists
Sessions remain after failures Teardown is skipped or does not use alwaysRun=true Put quit() in an always-run teardown and remove the thread-local reference
Element is intermittently missing Race with page navigation, asynchronous rendering, or overloaded infrastructure Wait for a meaningful condition, avoid arbitrary sleeps, and reduce load while diagnosing
Tests overwrite downloads or screenshots All workers use one path Include a unique test or thread identifier in every output path
Parallel run is slower than serial CPU, RAM, browser startup, Grid queue, or application capacity is saturated Measure each bottleneck and lower or rebalance concurrency
Remote session cannot connect Wrong Grid URL, port, firewall, or Grid process state Check the endpoint from the test machine, confirm Grid logs, and restrict firewall access appropriately
Order-dependent failures Tests rely on execution order or state left by another test Reset state per test and remove ordering assumptions; use grouping only where the dependency is intentional

9. Reliability and cleanup checklist

  • Every session has one owner and one teardown path.
  • Teardown runs after failed tests and removes per-thread references.
  • Test accounts, records, files, ports, and download directories are unique or safely locked.
  • Waits target application conditions rather than fixed delays.
  • Grid is reachable only from authorized networks.
  • Retries are used sparingly; a retry should not hide shared-state defects.
  • Logs identify suite, class, method, worker, browser, and Grid node.
  • Concurrency changes are compared using both elapsed time and failure rate.

10. Capture evidence without maintaining browser capture code

When parallel UI runs need screenshots or PDFs, you can capture them with Selenium yourself, but that adds browser startup, cleanup, consent handling, and storage work. ScreenshotNeo is a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF.

Or skip the browser setup

Use the API shown in the ScreenshotNeo docs:

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)
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}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account to get started.

11. Cost and performance considerations

  • TestNG itself does not make browser sessions free: each concurrent session consumes machine, browser, and Grid capacity.
  • More threads can reduce wall-clock time until CPU, RAM, slots, network, or the application becomes saturated.
  • Remote Grid adds transport and queue latency; measure from the test runner to the browser node.
  • Clean teardown prevents leaked sessions from consuming future capacity.
  • ScreenshotNeo charges only for clean shots; failed loads, bot checks, blank pages, timeouts, and cache hits cost nothing. Use its configurable caching TTL when repeated evidence does not need a fresh capture.

12. Frequently asked questions

What is the best parallel mode for most suites?

Use methods for genuinely independent methods. Use classes or tests when setup, fields, or parameters must stay grouped.

Does TestNG create one browser automatically for each thread?

No. Your setup code must create and own a WebDriver session for each concurrent execution context.

Can I use parallel TestNG without Selenium Grid?

Yes. TestNG can run multiple local browser sessions. Grid becomes useful when local capacity or browser and operating-system coverage is insufficient.

Is ThreadLocal required?

No. It is a common Java implementation for associating a driver with a worker thread. Any design that guarantees isolated sessions and reliable cleanup can work.

Why did increasing thread-count stop helping?

A resource is saturated or a dependency is serialized. Inspect CPU, RAM, browser startup, Grid slots, queue time, network, and application response time before adding more workers.

What should I check when defaults differ?

Check the TestNG version in your build. Thread-pool and data-provider controls, including documented defaults, can vary by version.