Thread Safety Is a Design Property
Shared mutable state is what makes code unsafe under threads. Break either half — make it unshared with ThreadLocal, or make it immutable — and put the driver behind an accessor before you ever need to.
The general rule behind all of this is worth stating plainly, because interviewers ask for it and it applies well beyond drivers.
Shared mutable state is what makes code unsafe under threads. Break
either half and you are safe. Make it unshared, which is the ThreadLocal
approach and by far the easiest for test frameworks. Or make it
immutable, like the Strings in Chapter 14 and the constants in Chapter 13,
because a value nobody can change cannot be corrupted by two threads.
So in a parallel suite: the driver goes in a ThreadLocal. Configuration
is read once and left unchanged, which is safe because it never changes.
Test data belongs to each test rather than to a shared list that tests add
to. And a static counter that several tests increment is a defect waiting
for the day someone raises the thread count — is static bad, then? No.
static is right for constants, for utility methods that hold no state,
and for the ThreadLocal holder itself. The danger is a static field
whose value changes while tests run. Static and unchanging is fine;
static and mutable is the problem. (Locks and synchronized are the tools
for genuinely sharing mutable data between threads — a well-designed test
suite avoids that need rather than managing it, and knowing they exist is
the right depth for an automation role.)
A framework keeps public static WebDriver driver; in BaseTest. The
suite is green and runs one test at a time, and it has worked for two
years — every page object reaches it with BaseTest.driver, and it is
simple. It has worked because it was never run in parallel. The day it
is, two tests write to that one field, and one starts driving the other's
browser. Fixing it then means it's threaded through every page object
already — a rewrite rather than a change. Put it in a ThreadLocal behind
a getDriver() method now, while there is one place to change. Everything
else keeps calling getDriver() and never knows. Put the driver behind an
accessor from day one, even single-threaded. Hiding how the driver is
stored is the encapsulation lesson from Chapter 7, and it turns a future
rewrite into a one-line change.
This is the standard DriverManager that appears in most professional
frameworks: a static final ThreadLocal<WebDriver> with setDriver,
getDriver, and unload methods, set called in setup and remove
called in teardown. Page objects and tests only ever call getDriver(),
so they neither know nor care how it is stored. TestNG and JUnit both run
parallel tests on a thread pool, which is why remove() matters — those
threads are reused. And the generics from earlier in this chapter are why
ThreadLocal<WebDriver> hands you back a WebDriver rather than an
Object you must cast.
High-value questions, especially for senior automation roles: why a
static WebDriver is unsafe in parallel execution (one field for the
whole program, so concurrent tests overwrite each other's driver); what
ThreadLocal does (holds a separate value per thread, so each test gets
its own driver); why call remove() (frameworks reuse pooled threads, so a
stale value reaches the next test and memory is retained); what type
erasure is (generic type information is used at compile time and largely
removed at run time); what makes code thread-safe (no shared mutable
state — make it unshared or immutable); and why use generics at all
(compile-time type checking and no casting).
Everything in this chapter comes down to one small, real class. The closing exercise shows it built wrong, one defect per earlier chapter, and then built right.