Java interview question
How do you make a Selenium framework thread-safe for parallel execution? Explain ThreadLocal.
Short answer
Never share a WebDriver between threads. Give each thread its own driver via ThreadLocal or injection scoped per test, avoid static mutable state, and isolate test data. ThreadLocal stores a separate value per thread, but you must call remove() in teardown because thread pools reuse threads.
Answer
The core rule is that a WebDriver instance must never be used by two threads at once: it represents one browser session, and its commands are not thread-safe. Parallel execution therefore needs one driver per thread (or per test), no static mutable state, and isolated test data. ThreadLocal is the classic way to give each thread its own driver while keeping a convenient global accessor.
What ThreadLocal does
A ThreadLocal<T> holds a separate value for each thread that accesses it. get() returns the value set by the current thread, so two threads calling DriverManager.get() receive different drivers.
public final class DriverManager {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
private DriverManager() {}
public static void start(String browser) {
WebDriver driver = switch (browser) {
case "firefox" -> new FirefoxDriver();
case "chrome" -> new ChromeDriver();
default -> throw new IllegalArgumentException("Unknown browser " + browser);
};
DRIVER.set(driver);
}
public static WebDriver get() {
WebDriver d = DRIVER.get();
if (d == null) throw new IllegalStateException("Driver not started on this thread");
return d;
}
public static void quit() {
WebDriver d = DRIVER.get();
try {
if (d != null) d.quit();
} finally {
DRIVER.remove(); // essential
}
}
}
Selenium Manager (Selenium 4.6+) resolves browser drivers automatically, so no driver-path setup is needed here.
Why remove() is essential
Test runners execute tests on pooled threads. If you only quit() and do not remove(), the thread keeps a reference to a dead driver, and the next test on that thread may get it, failing with “session ID is null” or similar. It is also a memory leak. Always clean up in a finally or an @AfterMethod(alwaysRun = true) / @AfterEach.
Wiring it to the runner
TestNG:
public abstract class BaseTest {
@BeforeMethod(alwaysRun = true)
@Parameters("browser")
public void setUp(@Optional("chrome") String browser) { DriverManager.start(browser); }
@AfterMethod(alwaysRun = true)
public void tearDown() { DriverManager.quit(); }
}
<suite name="ui" parallel="methods" thread-count="4"> ... </suite>
JUnit 5 enables parallelism in junit-platform.properties:
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
In JUnit 5 a cleaner alternative to ThreadLocal is an extension or a parameter resolver that creates a driver per test and injects it, which avoids hidden global state entirely. The same is true of DI-based frameworks.
Other sources of thread-unsafety
- Static fields in page objects or utilities (a static
WebDriverWait, a static “current user”). Make them instance fields. - Shared test data: two threads logging in as the same user, or editing the same order. Generate unique data per test or allocate from a pool.
- Non-thread-safe helpers:
SimpleDateFormat(usejava.time.format.DateTimeFormatter), sharedHashMaps in reporting code (useConcurrentHashMapor per-thread structures). - Reporting: ExtentReports and similar tools need a per-thread test node, often itself held in a
ThreadLocal. - Order dependence: tests that only pass in sequence will fail randomly in parallel.
Scaling out
Threads on one machine are limited by CPU and memory. Beyond that, point RemoteWebDriver at Selenium Grid 4 or a container-based grid and run more sessions in parallel across nodes.
What interviewers look for
- The one-driver-per-thread rule and why.
- Correct
ThreadLocalusage, especiallyremove()with thread pools. - Other shared-state hazards: static fields, test data, reporting.
- Runner configuration and how to scale beyond one machine.
A strong answer covers
- WebDriver is not thread-safe; one driver per thread or test
- Explains ThreadLocal: a separate value per thread
- Calls remove() in teardown to avoid leaks with thread pools
- Avoids static mutable state and shared test data
- Configures parallelism in TestNG or JUnit 5 and mentions Grid or containers