BaseTest is the most important class in your framework. Every test class extends it. It handles three critical jobs: creating the browser, configuring timeouts, and cleaning up after the test finishes. Think of it like the foundation of a building — every floor sits on top of it.
Without BaseTest, every test class would need to create its own WebDriver, set timeouts, maximize the window, and quit the browser. That is 15-20 lines of boilerplate copy-pasted into every test file. Change the browser setup once? Update 50 files. BaseTest eliminates this problem completely.
Q: What is the purpose of a BaseTest class in your framework?
A: BaseTest is the parent class for all test classes. It handles WebDriver initialization in @BeforeMethod, configuration loading, browser maximization, timeout settings, and browser cleanup in @AfterMethod. It also captures screenshots on test failure. By centralizing this logic, individual test classes only contain test logic — no setup/teardown code. If I need to change how the browser starts (add a new option, change timeout), I change it in one place and all 200+ tests get the update automatically.
When you run tests in parallel, TestNG creates multiple threads. If all threads share one WebDriver instance, they step on each other. Thread A clicks a button, Thread B reads the wrong page. ThreadLocal gives each thread its own copy of the driver. Problem solved.
package com.testerrank.base;
import com.testerrank.config.DriverFactory;
import com.testerrank.utils.ConfigReader;
import com.testerrank.utils.ScreenshotUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.ITestResult;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Optional;
import org.testng.annotations.Parameters;
import java.time.Duration;
public class BaseTest {
// ThreadLocal ensures each parallel thread gets its own driver
private static final ThreadLocal<WebDriver> driverThread =
new ThreadLocal<>();
protected WebDriverWait wait;
protected static final Logger logger =
LogManager.getLogger(BaseTest.class);
@BeforeSuite
public void loadConfig() {
// Load config once before entire suite runs
ConfigReader.init();
logger.info("Configuration loaded successfully");
}
@BeforeMethod
@Parameters({"browser"})
public void setUp(@Optional String browser) {
// Priority: testng.xml parameter > system property > config file
String selectedBrowser = browser != null ? browser
: System.getProperty("browser",
ConfigReader.get("browser", "chrome"));
boolean headless = Boolean.parseBoolean(
System.getProperty("headless",
ConfigReader.get("headless", "false")));
logger.info("Starting browser: " + selectedBrowser
+ " (headless: " + headless + ")");
// Create driver using factory pattern
WebDriver driver = DriverFactory.createDriver(
selectedBrowser, headless);
driverThread.set(driver);
// Configure browser
driver.manage().window().maximize();
driver.manage().timeouts().implicitlyWait(
Duration.ofSeconds(
ConfigReader.getInt("implicit.wait", 5)));
driver.manage().timeouts().pageLoadTimeout(
Duration.ofSeconds(
ConfigReader.getInt("page.load.timeout", 30)));
// Create explicit wait instance
wait = new WebDriverWait(driver,
Duration.ofSeconds(
ConfigReader.getInt("explicit.wait", 10)));
logger.info("Browser launched and configured");
}
@AfterMethod
public void tearDown(ITestResult result) {
WebDriver driver = getDriver();
// Screenshot on failure
if (result.getStatus() == ITestResult.FAILURE) {
logger.error("TEST FAILED: " + result.getName());
logger.error("Reason: "
+ result.getThrowable().getMessage());
if (ConfigReader.getBoolean(
"screenshot.on.failure", true)) {
String path = ScreenshotUtils.capture(
driver, result.getName());
logger.info("Screenshot saved: " + path);
}
} else if (result.getStatus() == ITestResult.SUCCESS) {
logger.info("TEST PASSED: " + result.getName());
} else {
logger.warn("TEST SKIPPED: " + result.getName());
}
// Always quit the browser
if (driver != null) {
driver.quit();
driverThread.remove(); // Prevent memory leak
logger.info("Browser closed");
}
}
// All test classes and page objects use this method
protected WebDriver getDriver() {
return driverThread.get();
}
protected void navigateTo(String path) {
String baseUrl = ConfigReader.get(
"base.url", "https://testerrank.com");
String url = baseUrl + path;
logger.info("Navigating to: " + url);
getDriver().get(url);
}
protected String getBaseUrl() {
return ConfigReader.get(
"base.url", "https://testerrank.com");
}
}Always call driverThread.remove() in tearDown. Without it, ThreadLocal holds a reference to the closed driver, causing memory leaks in long-running CI pipelines. This is a common issue in production frameworks that freshers miss.
BaseTest should not contain browser creation logic directly. That is the job of a DriverFactory. This follows the Factory design pattern — one method creates the right object based on input.
package com.testerrank.config;
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;
public class DriverFactory {
public static WebDriver createDriver(String browser,
boolean headless) {
switch (browser.toLowerCase()) {
case "firefox":
return createFirefox(headless);
case "edge":
return createEdge(headless);
case "chrome":
default:
return createChrome(headless);
}
}
private static WebDriver createChrome(boolean headless) {
ChromeOptions options = new ChromeOptions();
if (headless) {
options.addArguments("--headless=new");
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
options.addArguments("--window-size=1920,1080");
}
options.addArguments("--disable-notifications");
options.addArguments("--disable-popup-blocking");
return new ChromeDriver(options);
}
private static WebDriver createFirefox(boolean headless) {
FirefoxOptions options = new FirefoxOptions();
if (headless) {
options.addArguments("--headless");
}
return new FirefoxDriver(options);
}
private static WebDriver createEdge(boolean headless) {
EdgeOptions options = new EdgeOptions();
if (headless) {
options.addArguments("--headless=new");
}
return new EdgeDriver(options);
}
}Q: Why do you use ThreadLocal for WebDriver? What happens without it?
A: ThreadLocal ensures that each thread in parallel execution gets its own independent WebDriver instance. Without ThreadLocal, if I declare WebDriver as a regular instance variable and run tests in parallel, multiple test methods on different threads would share the same driver reference. Thread A might navigate to the login page while Thread B is trying to click a button on the dashboard — causing random, unpredictable failures. ThreadLocal acts like a per-thread variable container. Each thread reads and writes to its own copy, completely isolated from other threads.
Q: What design pattern does your DriverFactory use?
A: It uses the Factory design pattern. The createDriver method takes a browser name as input and returns the appropriate WebDriver instance — ChromeDriver, FirefoxDriver, or EdgeDriver. The caller (BaseTest) does not need to know the details of how each browser is configured. If we add Safari support tomorrow, we add one case to the factory — no changes needed in BaseTest or any test class.
In interviews at companies like Infosys, TCS, and Wipro, the first question about your framework is always about BaseTest. Practice explaining it in 2 minutes: "BaseTest handles driver creation using ThreadLocal for parallel safety, loads configuration from properties files, sets timeouts, and captures screenshots on failure in the tearDown method."
Key Point: BaseTest centralizes driver lifecycle management. ThreadLocal makes it parallel-safe. DriverFactory keeps browser creation logic clean and extensible.