Parallel Execution — Methods, Classes, Tests, Instances

9 min read

You got an introduction to TestNG in the Selenium course — this course goes much deeper. Running 200 Selenium tests sequentially takes 20–30 minutes. Running them with 4 parallel threads takes 5–8 minutes. Parallelism is one of the highest-leverage things you can add to a mature suite, but it is also the most common source of mysterious intermittent failures. The key is understanding what each parallel mode actually parallelises, what shared state it exposes, and the one data structure — ThreadLocal — that makes parallel Selenium tests correct. This lesson covers all four modes, the right thread-count for different environments, and the DriverManager pattern that most professional Selenium frameworks build on.

The four parallel modes

Set parallelism in testng.xml at the <suite> level:

<suite name="Parallel Suite" parallel="methods" thread-count="4">
ModeWhat runs concurrentlySafest for
noneNothing — fully sequential (default)Any starting point
methodsIndividual @Test methods across all classesStateless API tests
classesEntire test classesTests with @BeforeClass isolation
tests<test> blocks in testng.xmlCross-browser runs
instancesFactory-created instances@Factory cross-browser

parallel="methods" — finest granularity

Every @Test method from every class runs on any available thread:

<suite name="API Suite" parallel="methods" thread-count="4">
    <test name="All Tests">
        <packages>
            <package name="com.mycompany.tests.tests"/>
        </packages>
    </test>
</suite>

This is the fastest mode for stateless API tests where each @Test makes an HTTP call, asserts the response, and finishes — no shared mutable objects. For Selenium tests it requires ThreadLocal<WebDriver> because multiple test methods may run simultaneously on different threads.

Do not use this mode if test methods in the same class share a WebDriver instance field. Two methods accessing driver simultaneously will corrupt each other's browser sessions.

parallel="classes" — safer middle ground

All methods within one class run sequentially; different classes run concurrently:

<suite name="Selenium Suite" parallel="classes" thread-count="4">

If LoginTest and ProductTest are separate classes, they run on different threads. All methods inside LoginTest run sequentially on one thread; all methods inside ProductTest run sequentially on another. A WebDriver instance field in LoginTest is only ever accessed from one thread — no ThreadLocal needed, but only if you keep drivers as instance variables not static fields.

parallel="tests" — cross-browser pattern

<test> blocks run concurrently. The canonical use case is multi-browser:

<suite name="Cross Browser" parallel="tests" thread-count="3">
    <test name="Chrome">
        <parameter name="browser" value="chrome"/>
        <packages><package name="com.mycompany.tests.tests"/></packages>
    </test>
    <test name="Firefox">
        <parameter name="browser" value="firefox"/>
        <packages><package name="com.mycompany.tests.tests"/></packages>
    </test>
    <test name="Edge">
        <parameter name="browser" value="edge"/>
        <packages><package name="com.mycompany.tests.tests"/></packages>
    </test>
</suite>

All three browsers run concurrently. Within each <test> block the methods run sequentially (unless you add parallel="methods" to individual <test> tags). This is the least invasive way to add parallelism to an existing suite.

ThreadLocal<WebDriver> — making methods parallel-safe

ThreadLocal stores a separate value per thread. Each thread reads and writes its own copy:

package com.mycompany.tests.util;
 
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
 
public class DriverManager {
 
    private static final ThreadLocal<WebDriver> driver = new ThreadLocal<>();
 
    public static WebDriver getDriver() {
        return driver.get();
    }
 
    public static void initDriver(String browser) {
        WebDriver d = switch (browser.toLowerCase()) {
            case "firefox" -> new FirefoxDriver();
            case "edge"    -> new org.openqa.selenium.edge.EdgeDriver();
            default        -> new ChromeDriver();
        };
        d.manage().window().maximize();
        driver.set(d);
    }
 
    public static void quitDriver() {
        WebDriver d = driver.get();
        if (d != null) {
            d.quit();
            driver.remove();   // prevent memory leaks in long-running suites
        }
    }
}
package com.mycompany.tests.base;
 
import com.mycompany.tests.util.DriverManager;
import org.testng.annotations.*;
 
public class BaseTest {
 
    @BeforeMethod
    @Parameters("browser")
    public void setup(@Optional("chrome") String browser) {
        DriverManager.initDriver(browser);
    }
 
    protected org.openqa.selenium.WebDriver driver() {
        return DriverManager.getDriver();
    }
 
    @AfterMethod(alwaysRun = true)
    public void teardown() {
        DriverManager.quitDriver();
    }
}

Test classes use driver() instead of a field:

public class LoginTest extends BaseTest {
 
    @Test(groups = {"smoke"})
    public void loginFormIsVisible() {
        org.openqa.selenium.By locator = org.openqa.selenium.By.id("user-name");
        org.testng.Assert.assertTrue(driver().findElement(locator).isDisplayed());
    }
}

Choosing thread-count

General guidance for thread-count:

  • Local development: 2–4. Beyond 4 threads on a laptop, CPU and RAM contention slows things down.
  • CI (4-core runner): 4. Matches the physical cores; browser processes are CPU-bound.
  • CI (8-core runner): 6–8. Leave 1–2 cores for the OS and Maven overhead.
  • Selenium Grid: thread-count can match the number of Grid nodes — the grid handles the browser processes on remote machines.

Never set thread-count higher than your available browser capacity (local) or Grid slots (remote).

DataProvider parallelism

@DataProvider has its own parallel flag, independent of the suite's parallel setting:

@DataProvider(name = "apiCredentials", parallel = true)
public Object[][] credentials() {
    return new Object[][] {
        {"admin", "pass1"},
        {"user",  "pass2"},
        {"guest", "pass3"},
    };
}

Control the pool with data-provider-thread-count on the <suite>:

<suite name="Suite" data-provider-thread-count="4">

This only parallelises the invocations of the @Test method that uses this provider — it does not affect other tests.

Debugging parallel failures

Parallel failures are almost always race conditions on shared state. The debugging workflow:

  1. Set thread-count="1" in testng.xml. If the failure disappears, it's a race condition.
  2. Find shared mutable state: static fields, shared WebDriver, shared RestAssured request spec.
  3. Move to ThreadLocal (for stateful objects like WebDriver) or make the shared thing immutable (read-only config loaded in @BeforeSuite).
  4. Re-enable parallelism and rerun.

⚠️ Common mistakes

  • parallel="methods" with a shared WebDriver instance field. Two threads call @BeforeMethod simultaneously, both assign to the same driver field, then both tests interact with whichever driver they got — or both got the last one. The symptom is intermittent StaleElementReferenceException or NoSuchWindowException. Fix: ThreadLocal<WebDriver>.
  • Not calling driver.remove() in teardown. ThreadLocal stores values per thread. In a thread pool (which Surefire reuses across test methods), leftover values from a previous test are visible to the next test on the same thread. driver.remove() clears the thread-local after quitting the driver — always do both together.
  • Setting thread-count higher than CI core count. With 4 physical cores and thread-count="16", browsers compete for CPU time. The suite takes longer than with thread-count="4" because of context switching and memory pressure. Benchmark: run with 2, 4, and 8 threads and measure wall-clock time.

🎯 Practice task

Experience each parallel mode directly. 35–45 minutes.

  1. Start with parallel="none". Add System.out.println(Thread.currentThread().getName() + " running " + result.getMethod().getMethodName()) to @BeforeMethod. Run — all tests show the same thread name (main).
  2. Switch to parallel="classes" thread-count="2". Run again — you'll see two different thread names. Classes run in parallel; methods within a class still share a thread.
  3. Implement DriverManager with ThreadLocal<WebDriver>. Update BaseTest to use it. Run with parallel="methods" thread-count="4". Confirm all tests still pass — no thread collision.
  4. Diagnose a race condition. Remove ThreadLocal and use a plain WebDriver driver instance field in BaseTest. Run with parallel="methods" thread-count="4". Observe the failures. Restore ThreadLocal and confirm they disappear. This exercise makes the problem visceral.
  5. Set up a cross-browser.xml with parallel="tests" thread-count="2" and two <test> blocks. Confirm both blocks start simultaneously in the console.
  6. Stretch — DataProvider parallelism. Add @DataProvider(parallel = true) to a provider with 6 rows. Add Thread.sleep(300) inside the test. Compare run time with and without parallel = true — the parallel version should run in roughly 1/4 of the time.

Next lesson: TestNG listeners — ITestListener for screenshot capture and IReporter for custom HTML reports.

// tip to track lessons you complete and pick up where you left off across devices.