Answer
Parallel Test Execution with ThreadLocal WebDriver
Why ThreadLocal?
Without ThreadLocal, a shared static WebDriver causes thread interference:
CODE
Thread 1: opens Chrome → clicks Login
Thread 2: opens Chrome → OVERWRITES driver reference
Thread 1: tries to click → NullPointerException or wrong browser!
With ThreadLocal, each thread gets its own WebDriver:
CODE
Thread 1: driver = ChromeDriver #1 → clicks Login
Thread 2: driver = ChromeDriver #2 → independent
No interference!
DriverManager with ThreadLocal
Java
// driver/DriverManager.java
public class DriverManager {
// ThreadLocal stores separate driver per thread
private static ThreadLocal<WebDriver> driverThread = new ThreadLocal<>();
public static WebDriver getDriver() {
return driverThread.get();
}
public static void initDriver(String browser, boolean headless) {
WebDriver driver;
switch (browser.toLowerCase()) {
case "firefox":
FirefoxOptions ffOpts = new FirefoxOptions();
if (headless) ffOpts.addArguments("--headless");
driver = new FirefoxDriver(ffOpts);
break;
case "edge":
driver = new EdgeDriver();
break;
default: // chrome
ChromeOptions opts = new ChromeOptions();
if (headless) opts.addArguments(
"--headless=new", "--no-sandbox",
"--disable-dev-shm-usage", "--window-size=1920,1080");
driver = new ChromeDriver(opts);
}
driver.manage().window().maximize();
driver.manage().timeouts()
.implicitlyWait(Duration.ofSeconds(10));
driverThread.set(driver); // set for THIS thread only
}
public static void quitDriver() {
if (driverThread.get() != null) {
driverThread.get().quit();
driverThread.remove(); // IMPORTANT: prevent memory leak
}
}
}
BaseTest — Parallel-Safe Setup
Java
// base/BaseTest.java
public class BaseTest {
@BeforeMethod(alwaysRun = true)
public void setUp(Method method) {
String browser = ConfigReader.getBrowser();
boolean headless = ConfigReader.isHeadless();
DriverManager.initDriver(browser, headless);
DriverManager.getDriver().get(ConfigReader.getBaseUrl());
}
@AfterMethod(alwaysRun = true)
public void tearDown(ITestResult result) {
if (result.getStatus() == ITestResult.FAILURE) {
ScreenshotUtil.capture(result.getName());
}
DriverManager.quitDriver(); // quits THIS thread's driver
}
}
testng.xml — Enable Parallel Execution
XML
<!-- Run each test METHOD in parallel (max 3 at a time) -->
<suite name="Parallel Suite" parallel="methods" thread-count="3">
<test name="Regression">
<classes>
<class name="com.automateqa.tests.LoginTest"/>
<class name="com.automateqa.tests.SearchTest"/>
<class name="com.automateqa.tests.CheckoutTest"/>
</classes>
</test>
</suite>
<!-- OR: run each <test> block in parallel (different env/browser) -->
<suite name="Cross-Browser Suite" parallel="tests" thread-count="3">
<test name="Chrome Tests">
<parameter name="browser" value="chrome"/>
<classes><class name="com.automateqa.tests.SmokeTest"/></classes>
</test>
<test name="Firefox Tests">
<parameter name="browser" value="firefox"/>
<classes><class name="com.automateqa.tests.SmokeTest"/></classes>
</test>
<test name="Edge Tests">
<parameter name="browser" value="edge"/>
<classes><class name="com.automateqa.tests.SmokeTest"/></classes>
</test>
</suite>
Thread-Safe Extent Reports Logging
Java
// Use ThreadLocal for ExtentTest too!
public class TestListener implements ITestListener {
private static ExtentReports extent = ExtentManager.getInstance();
private static ThreadLocal<ExtentTest> test = new ThreadLocal<>();
public static ExtentTest getTest() { return test.get(); }
@Override
public void onTestStart(ITestResult result) {
test.set(extent.createTest(result.getMethod().getMethodName()));
}
@Override
public void onTestSuccess(ITestResult result) { test.get().pass("PASSED"); }
@Override
public void onTestFailure(ITestResult result) {
test.get().fail(result.getThrowable());
test.get().addScreenCaptureFromPath(ScreenshotUtil.capture(result.getName()));
}
@Override
public void onFinish(ITestContext context) { extent.flush(); }
}
