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">| Mode | What runs concurrently | Safest for |
|---|---|---|
none | Nothing — fully sequential (default) | Any starting point |
methods | Individual @Test methods across all classes | Stateless API tests |
classes | Entire test classes | Tests with @BeforeClass isolation |
tests | <test> blocks in testng.xml | Cross-browser runs |
instances | Factory-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
Parallel mode comparison — relative throughput for a 100-test Selenium suite
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-countcan 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:
- Set
thread-count="1"intestng.xml. If the failure disappears, it's a race condition. - Find shared mutable state: static fields, shared
WebDriver, sharedRestAssuredrequest spec. - Move to
ThreadLocal(for stateful objects likeWebDriver) or make the shared thing immutable (read-only config loaded in@BeforeSuite). - Re-enable parallelism and rerun.
⚠️ Common mistakes
parallel="methods"with a sharedWebDriverinstance field. Two threads call@BeforeMethodsimultaneously, both assign to the samedriverfield, then both tests interact with whichever driver they got — or both got the last one. The symptom is intermittentStaleElementReferenceExceptionorNoSuchWindowException. Fix:ThreadLocal<WebDriver>.- Not calling
driver.remove()in teardown.ThreadLocalstores 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-counthigher than CI core count. With 4 physical cores andthread-count="16", browsers compete for CPU time. The suite takes longer than withthread-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.
- Start with
parallel="none". AddSystem.out.println(Thread.currentThread().getName() + " running " + result.getMethod().getMethodName())to@BeforeMethod. Run — all tests show the same thread name (main). - 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. - Implement
DriverManagerwithThreadLocal<WebDriver>. UpdateBaseTestto use it. Run withparallel="methods" thread-count="4". Confirm all tests still pass — no thread collision. - Diagnose a race condition. Remove
ThreadLocaland use a plainWebDriver driverinstance field inBaseTest. Run withparallel="methods" thread-count="4". Observe the failures. RestoreThreadLocaland confirm they disappear. This exercise makes the problem visceral. - Set up a
cross-browser.xmlwithparallel="tests" thread-count="2"and two<test>blocks. Confirm both blocks start simultaneously in the console. - Stretch — DataProvider parallelism. Add
@DataProvider(parallel = true)to a provider with 6 rows. AddThread.sleep(300)inside the test. Compare run time with and withoutparallel = 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.