lecture 4 of 160 completed

Thread safety and parallel execution

The classic senior Selenium interview question, and the bug that makes parallel suites fail in ways nobody can reproduce.

A team enabled parallel execution and their suite started failing randomly. Different tests each run. Sometimes a click landed on the wrong page. Sometimes a test asserted on another test's data. Nothing reproduced in isolation.

They had one driver variable shared across threads. This is the most-asked senior Selenium interview question, and it is worth being able to explain precisely rather than just naming the fix.

Why sharing a driver breaks.

A WebDriver session is one conversation with one browser. Commands carry a session ID and are processed in the order they arrive.

Two threads sharing one driver interleave their commands into that single conversation:

Thread A: get('/contacts')
Thread B: get('/settings')        <- same browser, now on settings
Thread A: findElement(contactRow) <- looking for contacts on the settings page

Thread A's test fails, having done nothing wrong. And the failure depends on thread timing, so it differs every run. That is why it cannot be reproduced.

The fix: one driver per thread.

In Java, the canonical answer, and the one interviewers expect:

public class DriverManager {
    private static final ThreadLocal<WebDriver> driver = new ThreadLocal<>();

    public static WebDriver get() { return driver.get(); }

    public static void set(WebDriver d) { driver.set(d); }

    public static void quit() {
        if (driver.get() != null) {
            driver.get().quit();
            driver.remove();          // the line people forget
        }
    }
}

ThreadLocal gives each thread its own value under one variable name. Thread A's get() returns A's driver; B's returns B's.

driver.remove() matters more than it looks. Thread pools reuse threads. Without removing the value, a finished thread keeps a reference to a quit driver, and the next test on that thread receives a dead session. It also leaks memory. Being able to name this is a strong senior signal, because most candidates stop at ThreadLocal.

In JavaScript the problem takes a different shape. Node is single-threaded, and runners like Mocha achieve parallelism with separate processes. Separate processes do not share memory, so this specific bug cannot occur. Worth knowing, and worth saying in an interview: the answer differs by language, and understanding why matters more than reciting ThreadLocal.

What else must be thread-safe.

The driver is the obvious one. These are the ones that catch people out:

Page objects holding a driver. If a page object is created once and shared, it holds one thread's driver. Create page objects per test.

Anything static and mutable. A static currentUser, a shared counter, a cached config that tests mutate. Any static field written during a test is a race.

Test data. Two threads creating "Test User" at the same moment collide in the database. Thread isolation does not help, because the database is shared. This is the point that surprises people: making your driver thread-safe does not make your tests independent.

Reporting. Listeners writing to a shared file need synchronisation, or you get interleaved and corrupt output.

The rule that covers all of them: anything shared between tests must be either immutable or thread-local. There is no third safe option.

Verifying it. Run the suite with one thread and then with several. Tests that pass single-threaded and fail multi-threaded are showing you a shared-state bug, not a flaky test. That distinction is worth insisting on with your team, because the usual response to the second symptom is to add a retry, which hides a real defect.

Remember this: one driver per thread with ThreadLocal, remember remove(), and note that thread safety does not fix shared test data.

Reference: Parallel execution.