QA How-To
Java for Testers: Singleton driver factory (2026)
Build a java testers Singleton driver factory that creates isolated Selenium sessions, supports parallel TestNG runs, and guarantees safe driver cleanup.
21 min read | 2,460 words
TL;DR
A java testers Singleton driver factory should provide one shared creation policy, not one shared browser. Keep the factory stateless, let a lifecycle manager own one WebDriver per invocation or thread, and always quit and remove it during TestNG teardown. This design supports local and remote Selenium execution without cross-test session corruption.
Key Takeaways
- Make the factory singleton or stateless, but never make one mutable WebDriver session global across the suite.
- Create one WebDriver per test invocation or per deliberate scenario scope.
- Use `ThreadLocal<WebDriver>` only when TestNG setup, test execution, and teardown stay on the same thread.
- Call `quit()` in an always-run teardown and call `ThreadLocal.remove()` in a finally block.
- Pass WebDriver into page objects so their dependency and lifecycle remain visible.
- Keep browser options and local versus remote creation in a factory, separate from session ownership.
- Prove the design under parallel load by logging unique session IDs and checking for orphaned processes.
A java testers Singleton driver factory is safe only when "Singleton" describes the factory or configuration policy, not the Selenium browser session. WebDriver is mutable, stateful, and not thread-safe. Sharing one static driver among tests causes tab, cookie, navigation, and teardown collisions, especially when TestNG runs methods in parallel.
The reliable pattern has two parts: a stateless factory creates a new driver, and a lifecycle owner stores that driver for exactly one test invocation or thread. This guide builds both pieces with current Selenium 4 and TestNG APIs, explains ThreadLocal, and shows how to clean up without hiding dependencies.
TL;DR
| Component | Responsibility | Safe scope |
|---|---|---|
DriverFactory |
Translate browser configuration into a new driver | One stateless class or singleton service |
DriverSession |
Own the current driver and enforce start/get/stop | Per TestNG invocation thread |
WebDriver |
Hold one browser session's mutable state | Never global across parallel tests |
| Page object | Use a driver to model one page or component | Constructed for the current test |
| TestNG setup | Start the session before each method | @BeforeMethod |
| TestNG teardown | Capture evidence, quit, and remove | @AfterMethod(alwaysRun = true) |
If the suite is sequential, the same pattern still clarifies ownership. If it becomes parallel later, you do not need to redesign every page object.
1. Java Testers Singleton Driver Factory: Define Singleton Precisely
The classic Singleton pattern guarantees one instance of a class and a global access point. That can be reasonable for an immutable configuration or a stateless creation service. It is usually wrong for a browser. A Selenium session contains a current window, URL, cookie jar, timeouts, frame context, downloads, and server-side session ID. Every test action mutates it.
Consider two parallel tests sharing one static driver. Test A opens checkout while Test B navigates to account settings. Test A's next locator now executes on the settings page. If Test A fails and quits the driver, Test B receives NoSuchSessionException. Synchronizing every call would serialize execution and still mix authentication and page state.
Use these terms consistently:
- Driver factory: Knows how to construct Chrome, Firefox, Edge, or a remote session.
- Driver session manager: Owns the driver for a defined lifecycle.
- Driver singleton: One global mutable driver. Avoid this in a scalable suite.
- Singleton factory: One stateless service that creates many independent drivers. This is safe when its configuration is immutable.
The important design question is not, "How can every class call getDriver()?" It is, "Who creates this session, who may use it, and who guarantees its cleanup?" The Java automation framework architecture guide uses the same ownership principle for other shared services.
2. Why the Common Static WebDriver Singleton Fails
This familiar implementation is compact and unsafe:
public final class BadDriverSingleton {
private static WebDriver driver;
public static WebDriver getDriver() {
if (driver == null) {
driver = new ChromeDriver();
}
return driver;
}
}
It has several defects. The lazy check is a data race, so two threads can create two browsers and overwrite one reference. Even if creation is synchronized, all callers receive the same session. There is no explicit owner, no guaranteed quit, and no way to run different browsers or credentials concurrently. A previous test can leave cookies or local storage for the next test.
| Design | Parallel safe | Isolation | Cleanup clarity | Recommended |
|---|---|---|---|---|
| One static WebDriver | No | Poor | Poor | No |
| Synchronized global driver | No useful concurrency | Poor | Medium | No |
| New driver passed into each test | Yes | Excellent | Excellent | Yes |
ThreadLocal<WebDriver> manager |
Yes, with thread affinity | Good | Good if remove is enforced | Yes for TestNG thread model |
| Dependency injection with invocation scope | Yes | Excellent | Excellent | Yes for larger frameworks |
A global driver can appear stable while the suite is small and sequential. The cost arrives later as hidden coupling, order dependence, and painful parallel migration. Prefer explicit lifecycle even for ten tests.
Static factory methods are different because they hold no session. Calling DriverFactory.create(config) can be deterministic and thread-safe when it only creates and returns a new object.
3. Create an Immutable Browser Configuration
A factory should consume a validated configuration, not read system properties throughout a switch statement. This Java record makes browser, headless flag, and optional Grid endpoint explicit.
package example.browser;
import java.net.URI;
import java.util.Locale;
import java.util.Set;
public record BrowserConfig(String browser, boolean headless, URI gridUrl) {
private static final Set<String> SUPPORTED =
Set.of("chrome", "firefox", "edge");
public BrowserConfig {
browser = browser.toLowerCase(Locale.ROOT);
if (!SUPPORTED.contains(browser)) {
throw new IllegalArgumentException(
"Unsupported browser: " + browser + ", expected " + SUPPORTED);
}
if (gridUrl != null) {
String scheme = gridUrl.getScheme();
if (!("http".equals(scheme) || "https".equals(scheme))) {
throw new IllegalArgumentException(
"Grid URL must use http or https");
}
}
}
}
Construct the record once from a tested configuration loader. The Java config properties guide shows how to apply environment overrides and convert strings safely. Browser creation should not silently default a misspelled firfox to Chrome. Fail before the suite starts.
Keep browser-specific details near the factory: arguments, preferences, page load strategy, proxy, certificate behavior, and download directory. Keep scenario choices elsewhere. A test should not add --ignore-certificate-errors because one page object happens to need it. Such an option changes the session's security contract.
Do not expose mutable option objects globally. Build a fresh ChromeOptions, FirefoxOptions, or EdgeOptions for every driver. Selenium may augment capabilities during session negotiation, and shared mutable option instances make concurrent behavior difficult to reason about.
4. Build a Stateless Selenium WebDriver Factory
This factory creates a new driver on every call. It supports local Chrome, Firefox, and Edge, plus remote sessions using the same browser options. Selenium Manager can resolve local drivers when the environment supports it, so manual executable system properties are not required.
package example.browser;
import java.net.MalformedURLException;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
public final class DriverFactory {
private DriverFactory() {
}
public static WebDriver create(BrowserConfig config) {
Capabilities options = optionsFor(config);
if (config.gridUrl() != null) {
try {
return new RemoteWebDriver(config.gridUrl().toURL(), options);
} catch (MalformedURLException error) {
throw new IllegalArgumentException("Invalid Grid URL", error);
}
}
return switch (config.browser()) {
case "chrome" -> new ChromeDriver((ChromeOptions) options);
case "firefox" -> new FirefoxDriver((FirefoxOptions) options);
case "edge" -> new EdgeDriver((EdgeOptions) options);
default -> throw new IllegalStateException("Validated browser changed");
};
}
private static Capabilities optionsFor(BrowserConfig config) {
return switch (config.browser()) {
case "chrome" -> {
ChromeOptions options = new ChromeOptions();
if (config.headless()) options.addArguments("--headless=new");
yield options;
}
case "firefox" -> {
FirefoxOptions options = new FirefoxOptions();
if (config.headless()) options.addArguments("-headless");
yield options;
}
case "edge" -> {
EdgeOptions options = new EdgeOptions();
if (config.headless()) options.addArguments("--headless=new");
yield options;
}
default -> throw new IllegalStateException("Validated browser changed");
};
}
}
The casts are safe because the same validated browser chooses both options and local constructor. A larger framework can split these branches into provider classes to remove casts and add platform-specific capabilities. The key invariant remains: each call returns a distinct session or throws.
Avoid retrying driver creation blindly inside the factory. If a Grid capacity error is eligible for retry, make it an explicit policy with logging and a strict limit. Wrong URLs, unsupported capabilities, and missing browsers are deterministic configuration errors.
5. Own One Driver With a ThreadLocal Session Manager
ThreadLocal gives each Java thread its own value. TestNG generally runs a test method and its @BeforeMethod and @AfterMethod configurations on the same worker thread, which makes it a practical holder for method-scoped Selenium sessions. It is not a magic parallelism switch.
package example.browser;
import java.util.Objects;
import java.util.function.Supplier;
import org.openqa.selenium.WebDriver;
public final class DriverSession {
private static final ThreadLocal<WebDriver> CURRENT = new ThreadLocal<>();
private DriverSession() {
}
public static void start(Supplier<WebDriver> creator) {
if (CURRENT.get() != null) {
throw new IllegalStateException(
"A driver is already active on this thread");
}
CURRENT.set(Objects.requireNonNull(creator.get()));
}
public static WebDriver get() {
WebDriver driver = CURRENT.get();
if (driver == null) {
throw new IllegalStateException(
"No driver is active on this thread");
}
return driver;
}
public static void stop() {
WebDriver driver = CURRENT.get();
try {
if (driver != null) {
driver.quit();
}
} finally {
CURRENT.remove();
}
}
}
Do not use ThreadLocal.withInitial(DriverFactory::create) for browser sessions. An accidental getter call could silently open a browser outside setup, and a failure during creation is harder to associate with lifecycle. Explicit start and stop make ownership observable.
remove() is mandatory because TestNG reuses worker threads. Without it, the thread retains a closed driver reference and can retain the entire object graph, causing false reuse and memory leaks. The finally block ensures removal even when quit() throws.
6. Integrate Driver Lifecycle With TestNG
Start one session before each test method and stop it after each method. This scope gives strong isolation and makes retries safer because each attempt receives fresh method configuration.
package example.tests;
import example.browser.BrowserConfig;
import example.browser.DriverFactory;
import example.browser.DriverSession;
import java.net.URI;
import org.openqa.selenium.WebDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
public abstract class BaseUiTest {
private final BrowserConfig config = new BrowserConfig(
System.getProperty("browser", "chrome"),
Boolean.parseBoolean(System.getProperty("headless", "true")),
gridUrl());
@BeforeMethod(alwaysRun = true)
public void openBrowser() {
DriverSession.start(() -> DriverFactory.create(config));
}
@AfterMethod(alwaysRun = true)
public void closeBrowser() {
DriverSession.stop();
}
protected WebDriver driver() {
return DriverSession.get();
}
private static URI gridUrl() {
String raw = System.getProperty("grid.url");
return raw == null || raw.isBlank() ? null : URI.create(raw);
}
}
For publication clarity, this example reads three JVM properties directly. In a real framework, replace that construction with the validated configuration boundary from the earlier guide. In particular, strict boolean parsing should reject misspellings instead of treating them as false.
Teardown should capture screenshot, page source, console, and session metadata before calling stop. Wrap evidence collection so its failure never prevents quit. With retry analyzers, keep attempt artifacts under unique names rather than overwriting the first failure. See the TestNG retry analyzer guide for that reporting lifecycle.
7. Pass WebDriver Into Page Objects
A global getter inside every page object is convenient but hides the dependency. Prefer constructor injection. The test obtains its current driver once and supplies it to page objects.
package example.pages;
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public final class LoginPage {
private final WebDriver driver;
private final WebDriverWait wait;
public LoginPage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
public LoginPage open(String baseUrl) {
driver.get(baseUrl + "/login");
return this;
}
public void signIn(String username, String password) {
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("username")))
.sendKeys(username);
driver.findElement(By.id("password")).sendKeys(password);
driver.findElement(By.cssSelector("button[type='submit']")).click();
}
}
@Test
public void userCanSignIn() {
LoginPage login = new LoginPage(driver());
login.open("https://test.example.com")
.signIn("qa-user", "injected-secret");
}
In real code, credentials come from protected configuration, not literals. The example shows object ownership: the page receives a driver but does not create or quit it.
Constructor injection also makes page components testable and prevents a background helper thread from accidentally calling a thread-local getter that has no value. If a page action launches asynchronous Java work, pass only safe data to that work. Selenium commands should remain on the owning test thread.
8. Java Testers Singleton Driver Factory Under Parallel TestNG
Enable parallelism only after driver, data, reporting, and server accounts are isolated. A method-parallel suite can look like this:
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Parallel UI" parallel="methods" thread-count="4">
<test name="Chrome regression">
<packages>
<package name="example.tests"/>
</packages>
</test>
</suite>
Each worker runs @BeforeMethod, the test, and @AfterMethod; the thread-local holder provides its worker's session. Four threads can mean four simultaneous browsers, so match the count to local CPU and memory or remote Grid capacity. More threads can make the suite slower if the environment is saturated.
Add diagnostic logging at session start and stop. For RemoteWebDriver, log getSessionId() and the test method name without credentials or full capability secrets. Confirm that active session IDs are distinct during parallel runs. After the suite, Grid active sessions and local browser processes should return to baseline.
Avoid storing screenshots as failure.png. Parallel workers will overwrite it. Use a sanitized qualified method name, case ID, attempt number, and timestamp or unique ID. The same rule applies to downloads and temporary files.
If TestNG work crosses threads through custom executors, ThreadLocal values do not automatically follow. Do not use InheritableThreadLocal to spread a WebDriver session. Keep browser commands on the test worker or use explicit invocation-scoped objects managed by dependency injection.
9. Handle Startup Failures and Cleanup Defensively
A lifecycle manager must handle three states: no session, active session, and failed creation. In start, set the thread-local only after the factory returns a nonnull driver. If new ChromeDriver() throws, teardown sees no active session and safely removes the empty entry.
During teardown, use nested protection:
@AfterMethod(alwaysRun = true)
public void tearDown(ITestResult result) {
try {
if (!result.isSuccess()) {
captureFailureEvidence(DriverSession.get(), result);
}
} catch (RuntimeException evidenceError) {
System.err.println("Evidence capture failed: " + evidenceError);
} finally {
DriverSession.stop();
}
}
captureFailureEvidence is a framework extension point, not a Selenium API. Implement it using TakesScreenshot, page source, and your reporter. Guard DriverSession.get() if setup can fail, because no session may exist. A helper such as isStarted() can expose that state without returning null.
Do not reuse a driver after quit() and do not call only close(). close() closes the current window; quit() ends the complete WebDriver session and all associated windows. Calling quit() twice may throw depending on the implementation, so the lifecycle manager removes the reference after the first cleanup.
Shutdown hooks can be a final leak guard for local development, but they are not a replacement for method teardown. JVM termination may be abrupt, and a hook lacks the test context needed for reporting.
10. Verify and Evolve the Design
Test the factory separately from live browser journeys. Unit-test configuration validation and browser selection where practical. Run a small integration matrix that creates each supported local or remote driver, opens a controlled page, asserts the title, and quits. Keep that matrix distinct from business regression so environment provisioning failures are easy to identify.
For parallel verification, run several trivial methods with parallel="methods". Record thread ID, session ID, and test ID at setup and teardown. Assert operationally that no two simultaneous tests use the same session and every started session produces a stop event. Then repeat enough times to expose lifecycle races.
Review these signals over time:
- Browser startup duration and failures by browser and node.
- Active Grid sessions compared with TestNG worker count.
- Orphan browser processes after the run.
NoSuchSessionExceptionand cross-test navigation symptoms.- Evidence files with collisions or missing test IDs.
- Tests that require class-scoped reuse to pass.
If startup cost becomes material, do not jump to a global shared driver. First use API-based authentication, smaller smoke coverage, tuned worker counts, or safe class-level scope for a deliberately serial group. Session reuse trades speed for isolation and should be measured against contamination risk.
Add one failure-injection check to the lifecycle verification. Make the test body throw after the browser opens, then confirm that evidence handling runs, quit() is attempted, and the thread-local value is removed. Add another check in which evidence capture throws, because reporting code is a common reason cleanup is skipped. For remote execution, cancel a session from the Grid and verify that teardown still removes the local reference even if the server says the session no longer exists.
These checks test ownership rather than browser behavior. They should run in a small framework integration suite, not in every product pipeline. Combined with session event correlation, they give direct evidence that a future listener change or retry integration cannot strand a driver. Treat any unmatched start event as a framework defect even if all business assertions passed.
Interview Questions and Answers
Q: Is Selenium WebDriver thread-safe?
No. A WebDriver session is mutable and should be confined to one owning test flow. Parallel tests need independent sessions rather than synchronized access to one global driver.
Q: What should be Singleton in a driver factory design?
The stateless creation policy or immutable configuration may be shared. The WebDriver itself should be created per invocation or another explicit isolated scope.
Q: Why use ThreadLocal<WebDriver>?
It associates one driver with each TestNG worker thread, allowing static lifecycle access without sharing sessions across threads. It works only when setup, test, and teardown maintain thread affinity.
Q: Why call ThreadLocal.remove() after quit()?
TestNG reuses worker threads. Removing prevents the next test from retrieving a closed driver and releases references that could otherwise remain for the worker's lifetime.
Q: What is the difference between close() and quit()?
close() closes the current browser window. quit() terminates the entire WebDriver session and closes all its windows, which is the correct operation for final test cleanup.
Q: Should page objects call the driver factory?
No. A page object should receive an existing driver and model interactions. Creation and cleanup belong to the test lifecycle layer.
Q: How do you prove a driver manager is parallel-safe?
Run methods concurrently, log thread and session IDs, and verify each active test has a unique session and a matching cleanup event. Also check for orphan sessions and filename collisions after the suite.
Common Mistakes
- Calling one global WebDriver a Singleton and sharing it across tests.
- Synchronizing the shared driver and assuming that creates isolation.
- Creating a browser lazily from any
getDriver()call. - Forgetting
ThreadLocal.remove()after quitting. - Using
close()as final suite cleanup. - Letting page objects create or quit drivers.
- Sharing mutable browser options between threads.
- Starting browsers in constructors before TestNG lifecycle begins.
- Using the same account, download path, or screenshot filename in parallel.
- Sending Selenium commands from background executor threads.
- Hiding invalid browser names behind a Chrome default.
- Retrying deterministic startup configuration errors.
Conclusion
A java testers Singleton driver factory should centralize how drivers are created while producing an independent Selenium session for each test scope. Pair a stateless factory with explicit TestNG lifecycle ownership, use ThreadLocal only with known thread affinity, and always quit and remove in teardown.
Build the sequential lifecycle first, pass the driver into page objects, and then prove the design with a small parallel run. The outcome is more valuable than a convenient global getter: tests remain isolated, browser choices stay configurable, and every session has a clear owner from creation through cleanup.
Interview Questions and Answers
How would you design a Selenium driver factory for parallel TestNG tests?
I would keep driver construction stateless, validate browser configuration once, and create one session in `@BeforeMethod`. An invocation-scoped or thread-local manager would own it, and `@AfterMethod(alwaysRun = true)` would quit and remove it.
Why is a synchronized Singleton WebDriver still a bad parallel design?
Synchronization prevents simultaneous commands but does not isolate cookies, URLs, windows, or authentication. It also serializes the browser work, removing the intended parallel speedup.
What invariants should DriverSession enforce?
Starting twice on one thread should fail, getting before start should fail, and stopping should always quit then remove. A session created for one invocation must never be visible to another worker.
What are the limitations of ThreadLocal WebDriver?
It depends on thread affinity and creates hidden static access if used everywhere. Values do not follow arbitrary executor tasks, and forgetting removal leaks closed sessions into reused TestNG workers.
Where should browser options be configured?
A driver factory or browser-specific provider should build a fresh options object from immutable validated configuration. Individual page objects and tests should not mutate session-wide capabilities.
How would you handle a failure during driver startup?
I would store the driver only after successful construction, log sanitized environment and Grid evidence, and let teardown tolerate the no-session state. I would retry only a narrowly recognized transient capacity failure.
How do you detect leaked Selenium sessions?
I correlate test IDs, thread IDs, and remote session IDs at creation and cleanup. After the suite, active Grid sessions and local browser processes should return to baseline, and every start event should have a stop event.
Frequently Asked Questions
Should WebDriver be a Singleton in Selenium Java?
No global WebDriver should be shared across independent or parallel tests. The factory can be a singleton or stateless service, while it creates a separate driver for each explicit test scope.
How do I make WebDriver thread-safe in TestNG?
Do not share a session. Store one driver per worker with `ThreadLocal` or an invocation-scoped dependency, and ensure setup, test actions, and teardown remain on the owning thread.
When should I create and quit WebDriver in TestNG?
For strong isolation, create it in `@BeforeMethod` and quit it in `@AfterMethod(alwaysRun = true)`. Capture failure evidence before quitting.
Does Selenium Manager remove the need for a driver factory?
No. Selenium Manager helps resolve local driver binaries, but a factory still centralizes browser options, local versus remote creation, and configuration validation.
Why does ThreadLocal WebDriver return null?
The current thread has not called setup, setup failed before storing the driver, or code moved to another thread. Throw a clear lifecycle exception instead of allowing a later null pointer.
Can I reuse one browser to make Selenium tests faster?
You can choose a broader deliberate scope for a serial group, but reuse risks cookies, storage, windows, and application state leaking between tests. Measure safer alternatives such as API login and tuned parallelism first.
Should page objects use a static getDriver method?
Constructor injection is clearer because it exposes which session the page uses and prevents hidden thread-local access. The page object should never own driver creation or cleanup.