Q28 of 37 · Selenium

How would you handle parallel execution of Selenium tests across browsers?

SeleniumSeniorseleniumparalleltestngthread-localsenior

Short answer

Short answer: Run TestNG parallel at method/class level with `ThreadLocal<WebDriver>` to keep instances thread-safe. For multi-browser: use TestNG suites with a `<parameter name='browser'>`, run one suite per browser in CI, in parallel jobs. Selenium Grid scales out beyond one machine.

Detail

Parallel execution has two axes: within a browser (run tests concurrently), and across browsers (run the same suite on Chrome + Firefox + Safari). The right architecture handles both cleanly.

Within-browser parallelism with TestNG:

<suite name="Smoke" parallel="methods" thread-count="4">
  <parameter name="browser" value="chrome"/>
  <test name="Smoke">
    <packages><package name="com.example.tests"/></packages>
  </test>
</suite>

The non-negotiable: drivers must be thread-local:

private static final ThreadLocal<WebDriver> tl = new ThreadLocal<>();

Without that, threads share a driver and tests corrupt each other in spectacular ways. Page objects must be stateless or per-thread.

Cross-browser parallelism: don't try to fan-out browsers within one suite — it's brittle. Instead, run one suite per browser in parallel CI jobs:

# .github/workflows/e2e.yml
strategy:
  matrix:
    browser: [chrome, firefox, edge]
steps:
  - run: mvn test -Dbrowser=${{ matrix.browser }} -DsuiteXmlFile=testng.xml

Each matrix job is a separate TestNG run, gets its own report, and fails independently. CI aggregates the results.

Adding Grid: when you outgrow a single CI runner (typically ~16 concurrent browsers), add Grid. The matrix becomes "browser × shard," each shard hits the Grid which dispatches to nodes. Grid 4's Docker compose is the easy starting point.

The hidden cost: parallel tests amplify test-level statefulness. One test that depends on data another created will fail intermittently when they happen to run on the same node. The cleanest test suite has each test create and own its data; tests that share state need explicit serial groups (@Test(dependsOnMethods=...) or a separate non-parallel suite).

Common signs of poor parallelism: tests that pass alone but fail in the suite; failures that move between tests on each run; tests that mutate global state (a shared user, a tenant config).

// EXAMPLE

testng.xml

<?xml version="1.0" encoding="UTF-8"?>
<suite name="Smoke" parallel="methods" thread-count="6">
    <parameter name="browser" value="chrome"/>
    <parameter name="headless" value="true"/>

    <listeners>
        <listener class-name="com.example.listeners.RetryListener"/>
        <listener class-name="com.example.listeners.ScreenshotListener"/>
    </listeners>

    <test name="Smoke">
        <classes>
            <class name="com.example.tests.LoginTest"/>
            <class name="com.example.tests.CheckoutTest"/>
        </classes>
    </test>
</suite>

// WHAT INTERVIEWERS LOOK FOR

ThreadLocal driver as the safety boundary, separating within-browser parallelism (TestNG) from cross-browser (CI matrix), and naming test-data isolation as the hidden cost of parallelism.

// COMMON PITFALL

Trying to fan out browsers inside a single suite using TestNG <test> tags — brittle, hard to debug, and makes CI reporting murky. The matrix-job pattern scales much better.