How to Use the DriverManager ThreadLocal Pattern for Parallel Test Execution in Selenium
The DriverManager ThreadLocal pattern isolates WebDriver instances per thread by storing them in a ThreadLocal container, enabling thread-safe parallel test execution without race conditions.
The anhtester/automationframeworkselenium repository demonstrates a production-ready implementation of the DriverManager ThreadLocal pattern for Selenium WebDriver. This architecture ensures that when TestNG executes test methods in parallel, each thread maintains its own isolated browser session, preventing the race conditions and stale element exceptions that occur with shared static WebDriver instances.
Why ThreadLocal is Essential for Parallel Execution
When TestNG runs tests with parallel="methods", it spawns multiple Java threads simultaneously. A plain static WebDriver field would be shared across all threads, leading to race conditions, session collisions, and StaleElementReferenceException errors. The ThreadLocal class solves this by binding a driver instance to the specific thread that created it.
In src/main/java/com/anhtester/driver/DriverManager.java, the framework stores the WebDriver in a ThreadLocal<WebDriver> container. Any call to DriverManager.getDriver() from within a test method automatically retrieves the instance associated with the current thread, ensuring complete isolation between parallel tests.
Core Architecture Components
DriverManager.java
The DriverManager class in src/main/java/com/anhtester/driver/DriverManager.java serves as the central registry for WebDriver instances. It maintains a private static ThreadLocal<WebDriver> field and exposes three critical static methods:
setDriver(WebDriver driver)– Stores the driver instance into the ThreadLocal for the current threadgetDriver()– Retrieves the thread-bound driver instancequit()– Removes the driver from ThreadLocal and closes the browser session
This design pattern ensures that DriverManager.getDriver() always returns the correct driver for the thread executing the test, regardless of how many tests run simultaneously.
BaseTest.java
The BaseTest class in src/test/java/com/anhtester/common/BaseTest.java manages the driver lifecycle for every test method. It uses @BeforeMethod and @AfterMethod annotations to create and destroy drivers:
- Before each test: Calls
TargetFactory.createInstance()to build a fresh driver, wraps it with Selenium'sThreadGuard.protect()for additional thread safety, and registers it viaDriverManager.setDriver() - After each test: Invokes
DriverManager.quit()to clean up the thread-local session
This guarantees that each test method receives a pristine browser instance while maintaining thread isolation.
TargetFactory.java
Located in src/main/java/com/anhtester/driver/TargetFactory.java, the TargetFactory encapsulates driver instantiation logic. The createInstance(String browser) method determines whether to create a local driver via BrowserFactory or a remote driver for Selenium Grid/Docker execution. It returns a fresh WebDriver instance that BaseTest immediately binds to the current thread.
Configuring TestNG for Parallel Execution
To leverage the ThreadLocal pattern, configure TestNG to execute methods in parallel. The testng.xml configuration specifies the parallel mode and thread count:
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="ParallelSuite" parallel="methods" thread-count="4">
<test name="CRMTests">
<classes>
<class name="com.anhtester.projects.crm.testcases.LoginTest"/>
<class name="com.anhtester.projects.crm.testcases.CreateContactTest"/>
</classes>
</test>
</suite>
With parallel="methods" and thread-count="4", TestNG distributes test methods across four threads. Because each thread maintains its own driver in the ThreadLocal storage, tests execute concurrently without cross-thread contamination.
Implementing Thread-Safe Test Classes
Extending BaseTest for Automatic Driver Management
Test classes inherit thread-safe driver handling by extending BaseTest. All page objects and utility classes access the driver through DriverManager.getDriver(), which resolves to the thread-local instance:
package com.anhtester.projects.crm.testcases;
import org.openqa.selenium.By;
import org.testng.Assert;
import org.testng.annotations.Test;
import com.anhtester.common.BaseTest;
import com.anhtester.driver.DriverManager;
public class LoginTest extends BaseTest {
@Test
public void verifyLogin() {
DriverManager.getDriver().get("https://example-crm.com/login");
DriverManager.getDriver()
.findElement(By.id("username"))
.sendKeys("admin");
DriverManager.getDriver()
.findElement(By.id("password"))
.sendKeys("admin123");
DriverManager.getDriver()
.findElement(By.id("loginBtn"))
.click();
String greeting = DriverManager.getDriver()
.findElement(By.id("welcome"))
.getText();
Assert.assertTrue(greeting.contains("Welcome"));
}
}
The test inherits driver creation and cleanup logic from BaseTest, while DriverManager ensures that getDriver() returns the correct browser instance for the current thread.
Accessing Drivers in TestNG Listeners
The ThreadLocal pattern extends to TestNG listeners running in the same thread as the test. In src/test/java/com/anhtester/listeners/AllureListener.java, the listener captures screenshots on failure by accessing the thread-local driver:
public class AllureListener implements ITestListener {
@Override
public void onTestFailure(ITestResult result) {
if (DriverManager.getDriver() != null) {
byte[] screenshot = ((TakesScreenshot) DriverManager.getDriver())
.getScreenshotAs(OutputType.BYTES);
Allure.addAttachment(result.getName() + "_Failed",
new ByteArrayInputStream(screenshot));
}
}
}
Since the listener executes in the same thread as the failing test method, DriverManager.getDriver() correctly resolves to that test's browser instance.
Summary
- ThreadLocal isolation: The
DriverManagerclass inDriverManager.javausesThreadLocal<WebDriver>to bind driver instances to specific threads, preventing race conditions during parallel execution. - Lifecycle management:
BaseTest.javahandles driver creation viaTargetFactoryand registration throughDriverManager.setDriver(), ensuring each test method receives a fresh, thread-bound browser. - Concurrent safety: TestNG's
parallel="methods"configuration combined with ThreadLocal storage allows multiple tests to run simultaneously without session collisions or stale element errors. - Universal access: Page objects, utilities, and listeners throughout the codebase access the correct driver via
DriverManager.getDriver(), which automatically resolves to the current thread's instance.
Frequently Asked Questions
How does ThreadLocal prevent race conditions in parallel test execution?
ThreadLocal creates a separate variable copy for each thread accessing it. When TestNG runs tests in parallel, each thread calls DriverManager.setDriver() to store its own WebDriver instance. Subsequent calls to DriverManager.getDriver() return only that thread's specific driver, eliminating the possibility of one thread closing or interacting with another thread's browser session.
What is the role of ThreadGuard in the BaseTest implementation?
ThreadGuard.protect(), called in BaseTest.java before registering the driver, wraps the WebDriver instance with a proxy that verifies all driver interactions occur on the creating thread. This adds an extra layer of safety beyond ThreadLocal, throwing an exception if code attempts to use the driver from a different thread, which helps catch threading bugs during development.
Can this pattern support both local and remote WebDriver instances?
Yes. The TargetFactory.java class encapsulates the logic for creating both local drivers through BrowserFactory and remote drivers for Selenium Grid or Docker. Regardless of whether the driver runs locally or remotely, BaseTest stores the returned WebDriver in the ThreadLocal container via DriverManager.setDriver(), maintaining the same thread isolation guarantees.
How do you clean up WebDriver instances after parallel tests complete?
The DriverManager.quit() method removes the driver from the ThreadLocal map and calls driver.quit() to close the browser. In BaseTest.java, this method is invoked in an @AfterMethod block, ensuring that each thread's driver is destroyed immediately after its associated test method finishes, freeing up system resources and preventing memory leaks in long-running parallel suites.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →