Selenium โ Complete Interview Guide (5+ Yrs Automation Engineer)¶
Comprehensive Selenium reference grounded in your actual code at
/Users/rohan/ROhan personal/automation/B2BProjectTest/โ the Avysh B2B Java + Selenium 4 + TestNG + REST Assured + ExtentReports framework you built and ran in production for ~2 years. Every concept includes spoken-style interview answers and references real classes from your repo.
How this file is organised¶
| Section | Topic |
|---|---|
| 1 | Why Selenium + when to still pick it in 2026 |
| 2 | Architecture & internals (W3C, drivers, sessions) |
| 3 | WebDriver lifecycle + DriverFactory pattern |
| 4 | Locators โ basics, XPath, CSS, Selenium 4 relative locators |
| 5 | The three waits โ implicit, explicit, fluent |
| 6 | Actions (click, sendKeys, Actions class) |
| 7 | JavaScriptExecutor |
| 8 | Frames, windows, tabs, alerts, cookies |
| 9 | Page Object Model โ POM, Page Factory, Helper/Facade |
| 10 | TestNG annotations + listeners + retry |
| 11 | Parallel execution + ThreadLocal driver |
| 12 | Selenium Grid (local + Docker) |
| 13 | Selenium 4 CDP โ network, geolocation, console |
| 14 | Headless mode + CI flags |
| 15 | Exceptions โ what each one really means |
| 16 | Reporting โ ExtentReports listener pattern |
| 17 | Tech debt in B2BProjectTest โ how I'd refactor |
| 18 | Selenium โ Playwright migration (when and why) |
| 19 | 35+ interview Q&A with full spoken answers |
| 20 | Capacity planning & scale โ scenario/system-design questions (100 tests in 1 min, Safari constraint, reduce suite time, data isolation at scale) |
1. WHY SELENIUM¶
1.1 The honest 2026 framing¶
Selenium is no longer the default for new frameworks โ Playwright wins for greenfield work. But Selenium is still the right choice when:
| Reason | Why |
|---|---|
| Existing Java/TestNG codebase | Migration cost outweighs Playwright's wins |
| Legacy browser support (IE 11, older Edge) | Playwright doesn't target IE |
| Mature Selenium Grid + Docker fleet | Re-tooling parallel infra is expensive |
| Polyglot teams | Same Java API works for QA and backend devs |
| Compliance / banking | Selenium 4 is W3C-standardised; some procurement processes only allow W3C tools |
1.2 What Selenium 4 actually changed (memorize these โ top interview question)¶
| Change | What it gives you |
|---|---|
| W3C WebDriver protocol (replaces JSON Wire) | More reliable, standardized, faster |
Relative Locators (above, below, near, toLeftOf, toRightOf) |
Position-aware element finding |
| Native CDP support in Chrome / Edge | Network mocking, geolocation, device emulation, console listening |
New window/tab API โ driver.switchTo().newWindow(WindowType.TAB) |
Cleaner than the old window-handle dance |
Optional Capabilities replaced by browser-specific Options (ChromeOptions, FirefoxOptions) |
Type-safe configuration |
| Improved Grid โ Docker-native, observability, distributed mode | Easier scaling |
| Selenium Manager | Auto-downloads matching driver โ no more chromedriver version-pinning hell |
Spoken interview answer โ "What's new in Selenium 4?"¶
"Five things that actually matter in production. First, the protocol moved from JSON Wire to W3C WebDriver โ every browser now speaks the same standardized protocol, which made commands more reliable and slightly faster. Second, Relative Locators โ I can say
aboveOf,below,nearto find elements by position relative to another element, useful for forms where labels aren't formally associated. Third, native Chrome DevTools Protocol support โ I can throttle network, mock geolocation, intercept network requests directly without third-party plugins. Fourth, the new tab API โdriver.switchTo().newWindow(WindowType.TAB)is cleaner than juggling window handles. Fifth, Selenium Manager auto-downloads the right driver version, which killed an entire class of CI failures we used to debug weekly."
2. ARCHITECTURE & INTERNALS¶
2.1 The five layers¶
Your test code (Java/Python/JS/C#)
โ
Selenium Client Library (language-specific bindings)
โ
W3C WebDriver Protocol (HTTP + JSON)
โ
Browser Driver (chromedriver / geckodriver / msedgedriver)
โ
Browser (Chrome, Firefox, Edge, Safari)
2.2 What "session" means¶
When you call new ChromeDriver():
1. Client starts the chromedriver executable as a local HTTP server (port 9515 by default)
2. Sends POST /session with desired capabilities
3. chromedriver launches Chrome + attaches via DevTools
4. Returns a session ID
5. Every subsequent command (click, navigate, etc.) is an HTTP request with that session ID
Why this matters in interviews¶
"Each command in Selenium is a separate HTTP request to the driver. So
driver.findElement(...).click()is actually two requests โ one to resolve the locator, one to click. That's the architectural reason Selenium feels slower than Playwright's WebSocket โ at 200 commands per test, the per-request overhead adds up to seconds."
2.3 Why JSON Wire is gone¶
The old JSON Wire protocol was a Selenium-only spec. The W3C version standardised the wire format so browser vendors can implement it natively โ and they do. The endpoints and semantics are well-defined now, which made cross-browser tests behave more consistently.
3. WEBDRIVER LIFECYCLE + DRIVERFACTORY PATTERN¶
3.1 Your WebDriverUtils.java (real code, annotated)¶
public class WebDriverUtils {
public WebDriver driver;
public ThreadLocal<RemoteWebDriver> threadDriver = null;
public String runParallel = "false";
String driverPath = File.separator + "src" + File.separator + "main"
+ File.separator + "resources" + File.separator + "driver" + File.separator;
public WebDriver getDriver() {
if(threadDriver == null) {
return driver;
} else {
return threadDriver.get();
}
}
public void setDriverPath(String browserName) {
switch (browserName) {
case "Chrome":
if(System.getProperty("os.name").toLowerCase().contains("mac")) {
this.driverPath = driverPath + "mac" + File.separator + "chromedriver";
} else if(System.getProperty("os.name").toLowerCase().contains("windows")) {
this.driverPath = driverPath + "window" + File.separator + "chromedriver.exe";
} else {
this.driverPath = driverPath + "ubuntu" + File.separator + "chromedriver";
}
break;
// ... Firefox, IE, Edge, Safari ...
}
}
public void initializeDriver(String browserName, String hubURL, String mode) {
if(!runParallel.equalsIgnoreCase("true") && mode.equalsIgnoreCase("normal")) {
switch (browserName) {
case "chrome":
setDriverPath("Chrome");
System.setProperty("webdriver.chrome.driver",
System.getProperty("user.dir") + getDriverPath());
setDriver(new ChromeDriver());
break;
// ... Firefox, IE, Edge, Safari ...
}
} else if(!runParallel.equalsIgnoreCase("true") && mode.equalsIgnoreCase("headless")) {
// headless variant
ChromeOptions option = new ChromeOptions();
option.addArguments("--window-size=1920,1080");
option.addArguments("--start-maximized");
option.addArguments("--headless");
option.addArguments("--disable-extensions");
option.addArguments("--disable-gpu"); // legacy windows fix
option.addArguments("--disable-dev-shm-usage"); // /dev/shm too small in Docker
option.addArguments("--no-sandbox"); // Docker rootless workaround
setDriver(new ChromeDriver(option));
}
}
}
Spoken walkthrough โ "Walk me through WebDriverUtils"¶
"WebDriverUtils is the driver factory in our framework. It holds both a regular WebDriver and a
ThreadLocal<RemoteWebDriver>โ the ThreadLocal is what makes TestNG'sparallel="classes"mode work without browser instances stealing each other. ThesetDriverPathmethod picks the right binary per OS โ mac, windows, ubuntu โ because we kept the drivers insrc/main/resources/driver/{os}/.initializeDriveraccepts amodeparameter โ normal or headless โ because we ran headless on Jenkins CI but headed locally for debugging. The four Chrome flags--disable-gpu,--disable-dev-shm-usage,--no-sandbox,--disable-extensionsare the canonical CI quartet โ without them Chrome crashes in Docker. Each one has a specific reason:--no-sandboxis needed because Docker runs as root,--disable-dev-shm-usageis because/dev/shmis tiny by default,--disable-gpuis a legacy Windows fix that became habit,--disable-extensionsshaves seconds off startup."
3.2 The modern alternative โ Selenium Manager + WebDriverManager¶
The setDriverPath + manual System.setProperty pattern is legacy now. Selenium 4.6+ ships with Selenium Manager โ auto-downloads the right driver. You can also use the Bonigarcia WebDriverManager library (which your pom.xml already includes):
import io.github.bonigarcia.wdm.WebDriverManager;
WebDriverManager.chromedriver().setup();
WebDriver driver = new ChromeDriver();
Honest framing¶
"Our framework predates Selenium Manager โ when I built it in 2020, you really did have to ship per-OS chromedriver binaries in
resources/driver/{os}/. Today I'd delete the entire OS-detection block and use Selenium Manager. The WebDriverManager library is also in ourpom.xml, just unused โ I'd switch to it on the next refactor and remove about 80 lines of code fromWebDriverUtils."
3.3 The canonical headless flags โ what each one does¶
| Flag | Why |
|---|---|
--headless |
Run Chrome without a UI. --headless=new for Chrome 109+ |
--no-sandbox |
Bypass Chrome's user-namespace sandbox. Required when running as root in Docker. |
--disable-dev-shm-usage |
Force Chrome to use /tmp for shared memory instead of /dev/shm (which is only 64MB in default Docker) |
--disable-gpu |
Legacy Windows fix (Chrome on Windows tried to use GPU for headless rendering and crashed). Mostly harmless on Linux. |
--window-size=1920,1080 |
Set a deterministic viewport for screenshots and responsive selectors |
--start-maximized |
Maximizes window โ useful for layouts that depend on viewport size |
--disable-extensions |
Faster startup, avoids extension interference |
4. LOCATORS โ DEEP¶
4.1 The eight built-in locators¶
By.id("email"); // fastest, requires unique id
By.name("password"); // form fields
By.className("btn-primary"); // CSS class
By.tagName("input"); // when only one tag matches
By.linkText("Sign in"); // exact link text
By.partialLinkText("Sign"); // substring link text
By.cssSelector("input[name='email']"); // fast, flexible
By.xpath("//input[@id='email']"); // last resort
4.2 Priority order I follow¶
- ID โ fastest because browsers maintain an
id โ elementmap. Use if available. - Name โ form fields often have stable
name - CSS selector โ fast, flexible, well-supported
- Link text โ for anchors only
- XPath โ last resort, but unavoidable when text-based lookup is needed
4.3 XPath cheatsheet (the parts that show up in interviews)¶
//input[@type='text'] attribute equality
//button[text()='Login'] exact text
//button[contains(text(),'Log')] partial text
//div[@class='card' and @data-id='42'] multi-attribute
//div[contains(@class,'btn-primary')] partial class (because @class is whole string!)
//a[starts-with(@href,'/user')] starts-with
//div[@id='form']//input descendant
//div[@id='form']/input direct child
//input[1] first matching
//input[last()] last matching
//tr[td[contains(.,'Pending')]] rows containing td with 'Pending'
//label[normalize-space()='Email']/following-sibling::input sibling navigation
//input[@id='email']/ancestor::form walk up the tree
Real example from your HomePage.java¶
@FindBy(xpath="//*[@webix_tm_id='channelparent']")
WebElement linkManageChannels;
@FindBy(xpath="//*[@webix_tm_id='sidebar_link_LevelsTiers']")
WebElement linkManageChannelsLevels;
@FindBy(xpath="//*[@view_id='$template7']/div/div/span")
WebElement orderStatus;
webix_tm_id, view_id, radio_id attributes. These are stable per-feature, and By.cssSelector("[webix_tm_id='channelparent']") would work equally โ but the team standardized on XPath because the Webix tree often required parent/child navigation that's cleaner in XPath.
Spoken answer โ "When do you use XPath vs CSS?"¶
"CSS first when the locator is a simple attribute or descendant relationship โ it's faster and more readable. XPath when I need text-based matching (
contains(text(), ...)) or navigation up the tree (ancestor::form) or by sibling (following-sibling::input). In our B2B framework we used XPath heavily because the underlying UI framework, Webix, exposed custom attributes โwebix_tm_id,view_idโ and the layouts had complex parent-child relationships. I never write absolute XPaths like/html/body/div[1]/...โ those break on the first DOM change."
4.4 Selenium 4 Relative Locators¶
import static org.openqa.selenium.support.locators.RelativeLocator.with;
WebElement emailLabel = driver.findElement(By.id("email-label"));
WebElement emailInput = driver.findElement(with(By.tagName("input")).toRightOf(emailLabel));
// Available relations: above, below, toLeftOf, toRightOf, near
4.5 PageFactory @FindBy (your B2BProjectTest pattern)¶
public class MyOrdersPage extends CommonUtils {
@FindBy(xpath="//*[@class='viewOrder']")
WebElement btnViewEye;
@FindBy(xpath="//*[@view_id='orderAccept']/div/button")
WebElement btnAccept;
@FindBy(xpath="//*[@view_id='orderReject']/div/button")
WebElement btnReject;
public MyOrdersPage(WebDriver driver) {
super(driver);
PageFactory.initElements(driver, this);
}
}
How @FindBy works:
1. PageFactory.initElements(driver, this) scans the class via reflection for @FindBy-annotated fields
2. For each field, it creates a proxy WebElement that captures the locator
3. The proxy doesn't resolve the element until you actually call a method on it (lazy)
4. Every call re-resolves โ so cached references don't go stale between actions
Spoken answer โ "What is PageFactory and how is it different from manual findElement?"¶
"PageFactory is a Selenium utility that uses reflection to wire up
@FindBy-annotated fields with proxy WebElements. Two practical effects. First, the locator and its declaration sit next to the field name โ readability win. Second, the proxy is lazy and re-resolves on each call, so I rarely hit StaleElementReferenceException unless the parent DOM literally is replaced. The trade-off is that PageFactory's proxies can mask issues โ when the locator is wrong, you get NoSuchElementException only when you actually use it, not when the page object is constructed. For complex pages with hundreds of locators that's fine; for simple flows, manualdriver.findElementis honestly easier to debug."
5. THE THREE WAITS โ IMPLICIT, EXPLICIT, FLUENT¶
This is the single most-asked Selenium topic. Memorize the differences.
5.1 Implicit wait¶
- Sets a global timeout for everyfindElement call
- If element not immediately found, polls until found or timeout
- Once set, applies to every find for the driver's lifetime
- Downside: applies to every find, which makes negative tests (asserting absence) slow
5.2 Explicit wait¶
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("submit")));
wait.until(ExpectedConditions.elementToBeClickable(By.id("submit")));
wait.until(ExpectedConditions.textToBePresentInElementLocated(
By.id("status"), "Success"));
TimeoutException if condition not met
- This is what you should use in production code
Common ExpectedConditions¶
| Condition | Use when |
|---|---|
visibilityOf(elem) |
Wait for element to be visible in DOM and have non-zero size |
visibilityOfElementLocated(by) |
Same but with locator (re-resolves) |
elementToBeClickable(elem) |
Visible AND enabled |
presenceOfElementLocated(by) |
In DOM (may be hidden) |
invisibilityOfElementLocated(by) |
Spinner gone |
textToBePresentInElement(elem, text) |
Element contains text |
urlContains(fragment) |
After navigation |
titleContains(title) |
After navigation |
alertIsPresent() |
Before driver.switchTo().alert() |
numberOfElementsToBe(by, 5) |
Wait for list of 5 |
stalenessOf(elem) |
Wait for element to detach from DOM |
5.3 Fluent wait¶
Wait<WebDriver> wait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(30))
.pollingEvery(Duration.ofSeconds(2))
.ignoring(NoSuchElementException.class)
.ignoring(StaleElementReferenceException.class);
WebElement el = wait.until(d -> d.findElement(By.id("data")));
5.4 The Big Anti-Pattern โ mixing implicit and explicit¶
// DON'T DO THIS
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("x")));
Spoken answer โ "Walk me through the three waits and when to use each"¶
"Implicit wait is global โ set once, applies to every
findElementcall. The downside is it slows down negative tests because every 'element not found' check waits the full timeout. Explicit wait is targeted โ it waits for a specific condition on a specific element usingWebDriverWaitplusExpectedConditions.elementToBeClickableor similar. This is what I use 95% of the time. Fluent wait is explicit wait plus a custom polling interval and the ability to ignore specific exceptions during polling โ useful when I expect transientStaleElementReferenceExceptionduring a DOM refresh. The cardinal rule is never mix implicit and explicit โ set implicit to zero and use explicit everywhere. Mixing them produces unpredictable timeouts."
5.5 Thread.sleep is not a wait¶
Your WebDriverUtils.waitForPageToCompleteState() uses Thread.sleep inside a loop:
public void waitForPageToCompleteState() throws InterruptedException {
int counter = 0;
int maxNoOfRetries = 10;
while (maxNoOfRetries > 0 && (counter != maxNoOfRetries)) {
Thread.sleep(2000);
try {
JavascriptExecutor js = (JavascriptExecutor) driver;
if (js.executeScript("return document.readyState").toString().equals("complete")) {
Thread.sleep(2000);
break;
}
} catch (Exception e) {}
counter++;
}
}
document.readyState with Thread.sleep retries โ anti-pattern on three counts. First, Thread.sleep is a blocking pause, not a wait. Second, the Thread.sleep(2000) after the check is a magic delay that means the function always takes at least 2s even when the page was ready. Third, the outer Thread.sleep(2000) per loop means worst-case 20 seconds for a ready page that has a glitch in readyState.
Right pattern:
new WebDriverWait(driver, Duration.ofSeconds(20))
.until(d -> ((JavascriptExecutor) d).executeScript("return document.readyState").equals("complete"));
6. ACTIONS โ CLICK, SENDKEYS, ACTIONS CLASS¶
6.1 Basic interactions¶
WebElement input = driver.findElement(By.id("email"));
input.clear();
input.sendKeys("a@x.com");
driver.findElement(By.id("submit")).click();
driver.findElement(By.id("country")).getAttribute("value");
driver.findElement(By.id("desc")).getText();
6.2 Your CommonUtils.click pattern (real code)¶
public void click(WebElement element) throws Exception {
try {
waitTillElementIsClickable(element, minTime);
element.click();
} catch (StaleElementReferenceException ex) {
waitTillElementIsClickable(element, minTime);
element.click();
}
}
public void sendKeysTo(WebElement element, String str) throws InterruptedException {
element.clear();
element.sendKeys(str);
}
waitTillElementIsClickable before every click means tests don't fail on transient timing. The Stale catch-and-retry handles the case where the DOM updates between the wait and the click โ rare but real on Angular apps that re-render on data load.
6.3 Select dropdowns¶
import org.openqa.selenium.support.ui.Select;
Select dropdown = new Select(driver.findElement(By.id("country")));
dropdown.selectByVisibleText("India");
dropdown.selectByValue("IN");
dropdown.selectByIndex(2);
// Read
String selected = dropdown.getFirstSelectedOption().getText();
List<WebElement> options = dropdown.getOptions();
<select> elements. Custom div-based dropdowns (like Webix in your B2B app) need click-open + click-option.
6.4 Actions class โ mouse, keyboard, drag¶
import org.openqa.selenium.interactions.Actions;
Actions actions = new Actions(driver);
// Hover
actions.moveToElement(menu).perform();
// Right-click
actions.contextClick(elem).perform();
// Double-click
actions.doubleClick(elem).perform();
// Drag and drop
actions.dragAndDrop(source, target).perform();
// Manual drag (for HTML5 dragstart events)
actions.clickAndHold(source).moveByOffset(100, 0).release().perform();
// Keyboard
actions.keyDown(Keys.CONTROL).click(link).keyUp(Keys.CONTROL).perform(); // Ctrl+click
actions.sendKeys(Keys.chord(Keys.CONTROL, "a")).perform(); // Ctrl+A
6.5 File upload¶
Works because Selenium types the path into the hidden<input type="file">. No native dialog interaction.
6.6 Real-world clear quirk¶
element.clear() doesn't always fire input / change events that React or Angular listen on. If clear doesn't trigger the form's dirty state, do:
7. JAVASCRIPTEXECUTOR¶
When WebDriver can't do it natively, drop down to JS.
7.1 Common uses¶
JavascriptExecutor js = (JavascriptExecutor) driver;
// Scroll
js.executeScript("window.scrollTo(0, document.body.scrollHeight);");
js.executeScript("arguments[0].scrollIntoView({block: 'center'});", element);
// Click when an overlay intercepts
js.executeScript("arguments[0].click();", element);
// Read storage
String token = (String) js.executeScript("return localStorage.getItem('access_token');");
// Set a hidden input bypassing oninput handlers
js.executeScript("arguments[0].value = arguments[1];", input, "hello");
// Set viewport for screenshot fidelity
js.executeScript("window.resizeTo(1920, 1080);");
// Get page title via DOM (more reliable than driver.getTitle on some apps)
String title = (String) js.executeScript("return document.title;");
// Wait for jQuery / Angular to be idle
Boolean done = (Boolean) js.executeScript("return jQuery.active === 0;");
7.2 When to use vs not use¶
- Use when overlay intercepts a click, when scroll is needed for a fixed-header layout, when reading browser storage
- Don't use for things WebDriver supports natively โ JS-click bypasses Selenium's actionability checks and can mask real bugs (e.g., the button was disabled but JS click still triggered the handler)
Spoken answer โ "When would you use JavascriptExecutor?"¶
"Three real scenarios. First, when an overlay intercepts a click โ
arguments[0].click()via JS bypasses the click interceptor. Second, when I need to scroll an element into the visible viewport, especially with sticky headers,scrollIntoViewwithblock: 'center'is more reliable than Selenium's auto-scroll. Third, reading browser state that WebDriver doesn't expose โ localStorage tokens, performance.now() timings, anything inwindow. I avoid using it as a workaround for click failures, because JS click skips Selenium's actionability checks โ visibility, enabled, hit-testing โ which can mask bugs like 'button is disabled but the handler still fires.'"
8. FRAMES, WINDOWS, TABS, ALERTS, COOKIES¶
8.1 iframes¶
// By index, name/id, or element
driver.switchTo().frame(0);
driver.switchTo().frame("paymentFrame");
driver.switchTo().frame(driver.findElement(By.tagName("iframe")));
driver.switchTo().parentFrame(); // back one level
driver.switchTo().defaultContent(); // back to top
switchTo().defaultContent() and your next locator silently fails.
8.2 Multiple windows / tabs (the old way)¶
String original = driver.getWindowHandle();
// click something that opens a new tab
for (String handle : driver.getWindowHandles()) {
if (!handle.equals(original)) {
driver.switchTo().window(handle);
break;
}
}
// ... work in new tab
driver.close(); // closes current window
driver.switchTo().window(original);
8.3 New tab/window โ Selenium 4 native¶
driver.switchTo().newWindow(WindowType.TAB);
driver.switchTo().newWindow(WindowType.WINDOW);
// Driver now points at the new window โ no handle juggling
8.4 Alerts (browser-native popup)¶
Alert alert = driver.switchTo().alert();
String text = alert.getText();
alert.accept(); // OK
alert.dismiss(); // Cancel
alert.sendKeys("answer"); // for prompts
8.5 Cookies โ manipulate without UI¶
// Save
File f = new File("cookies.json");
new ObjectMapper().writeValue(f, driver.manage().getCookies());
// Load
driver.get("https://example.com"); // must be on the domain first
List<Cookie> saved = new ObjectMapper().readValue(f, new TypeReference<>(){});
saved.forEach(c -> driver.manage().addCookie(c));
driver.navigate().refresh();
// One-off
driver.manage().addCookie(new Cookie("session", "abc123"));
driver.manage().deleteCookieNamed("session");
driver.manage().deleteAllCookies();
storageState โ login once, save cookies, reuse to skip the login form in subsequent tests.
9. PAGE OBJECT MODEL โ POM, PAGE FACTORY, HELPER/FACADE¶
9.1 Plain POM¶
public class LoginPage {
private final WebDriver driver;
private final By emailInput = By.id("email");
private final By passwordInput = By.id("password");
private final By loginBtn = By.id("login");
public LoginPage(WebDriver driver) { this.driver = driver; }
public void login(String email, String password) {
driver.findElement(emailInput).sendKeys(email);
driver.findElement(passwordInput).sendKeys(password);
driver.findElement(loginBtn).click();
}
}
9.2 Page Factory POM (your B2BProjectTest pattern)¶
public class HomePage extends CommonUtils {
public WebDriver driver;
@FindBy(xpath="//*[@webix_tm_id='channelparent']")
WebElement linkManageChannels;
@FindBy(xpath="//*[@webix_tm_id='ordersparent']")
WebElement linkOrders;
public HomePage(WebDriver driver) {
super(driver);
this.driver = driver;
PageFactory.initElements(driver, this);
}
}
9.3 Your three-tier pattern โ Page + Helper + Test¶
You're using a sophisticated layering โ Page Objects expose elements, Helper classes wrap user-facing business actions, Tests call Helpers.
// PAGE (locators + atomic actions)
public class HomePage extends CommonUtils { @FindBy(...) WebElement linkOrders; }
// HELPER (business workflows)
public class HomeHelper {
HomePage homePage;
public HomeHelper(WebDriver driver) { homePage = new HomePage(driver); }
public void selectBrand(String brand, String subBrand) {
homePage.clickBrandDropdown();
homePage.selectBrand(brand);
// ...
}
}
// TEST (orchestrates helpers)
public class RegionsTest {
@Test
public void verifyCreateAndDeleteRegions() throws Exception {
homeHelper.selectBrand(testData.get("brandName"), testData.get("subBrandName"));
regionsHelper.createRegions(testData.get("countryName"), testData.get("cityName"));
softAssertions.assertThat(regionsHelper.verifyCreateRegions(testData.get("cityName")))
.describedAs("Regions Created Successfully")
.isEqualTo(true);
}
}
Spoken answer โ "Why both Page and Helper layers?"¶
"The Page layer owns the DOM โ locators and atomic actions. The Helper layer owns the user journey โ
selectBrand,createRegion,placeOrder. Tests orchestrate the helpers. The win is that tests read like English โregionsHelper.createRegions(country, city)โ and changes to the page structure only ripple to the Page class, not every test. The Helper layer also lets multiple tests share a workflow without duplicating it. The trade-off is one more layer to navigate, which is fine for a 40+ page application like ours but overkill for a 5-page app."
9.4 Inheritance pattern โ extending a CommonUtils base¶
Your pages extend CommonUtils which holds driver, click(), sendKeysTo(), findTheElement(), alertPopUp() etc. This is the Base Page pattern โ shared helpers in one place.
public abstract class CommonUtils {
public WebDriver driver;
public CommonUtils(WebDriver driver) { this.driver = driver; }
public void click(WebElement element) throws Exception {
try {
waitTillElementIsClickable(element, minTime);
element.click();
} catch (StaleElementReferenceException ex) {
waitTillElementIsClickable(element, minTime);
element.click();
}
}
public void sendKeysTo(WebElement element, String str) { ... }
public void waitForNGToLoad() { ... }
public void alertPopUp() { ... }
}
10. TESTNG ANNOTATIONS + LISTENERS + RETRY¶
10.1 The annotation order โ memorize this¶
@BeforeSuite โ once before all tests
@BeforeTest โ before each <test> in testng.xml
@BeforeClass โ once per test class
@BeforeMethod โ before each @Test
@Test โ the test
@AfterMethod โ after each @Test
@AfterClass โ once per test class
@AfterTest โ after each <test>
@AfterSuite โ once after all tests
10.2 The full set of @Test attributes¶
@Test(
description = "Create and Delete a Region", // shows in reports
groups = {"Smoke_Test"}, // for selective runs
testName = "Regions-1.1", // human-readable name
priority = 1, // lower runs first
enabled = true, // skip if false
invocationCount = 3, // run N times
threadPoolSize = 2, // run invocations in parallel
timeOut = 60_000, // ms per test
dependsOnMethods = "verifyLogin",
dependsOnGroups = "auth",
alwaysRun = true, // run even if dependencies failed
dataProvider = "users",
expectedExceptions = NoSuchElementException.class
)
public void verifyCreateAndDeleteRegions() throws Exception { ... }
Your real test (annotated)¶
@Test(description = "Create and Delete a Region",
groups = {"Smoke_Test"},
testName = "Regions-1.1")
public void verifyCreateAndDeleteRegions() throws Exception {
try {
softAssertions = new SoftAssertions();
Map<String, String> testData = new ReadTestData()
.readJsonElementInOrder("regions/regions.json", "verifyCreateAndDeleteRegions");
homeHelper.selectBrand(testData.get("brandName"), testData.get("subBrandName"));
regionsHelper.createRegions(testData.get("countryName"), testData.get("cityName"));
softAssertions.assertThat(regionsHelper.verifyCreateRegions(testData.get("cityName")))
.describedAs("Regions Created Successfully")
.isEqualTo(true);
regionsHelper.deleteRegions(testData.get("cityName"));
softAssertions.assertThat(regionsHelper.verifyCreateRegions(testData.get("cityName")))
.describedAs("Regions Created Successfully")
.isEqualTo(false);
softAssertions.assertAll();
} catch (Exception e) {
webDriverUtils.goToHome(properties.getProperty("homeUrl"));
throw new CustomException(e, driver);
}
}
.describedAs for descriptive failure messages. Custom exception wrapper that takes a screenshot and includes context.
10.3 DataProvider¶
@DataProvider(name = "loginData")
public Object[][] loginData() {
return new Object[][] {
{ "valid@x.com", "Pass@123", true },
{ "invalid", "Pass@123", false },
{ "", "", false },
};
}
@Test(dataProvider = "loginData")
public void testLogin(String email, String pwd, boolean expectedSuccess) { ... }
Parallel DataProvider¶
@DataProvider(name = "users", parallel = true)
public Object[][] users() { ... }
// All rows run in parallel โ each in its own thread
10.4 Listeners โ your ExtentReporterNG and Retry¶
ExtentReporterNG implements IReporter¶
public class ExtentReporterNG implements IReporter {
private ExtentReports extent;
public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites,
String outputDirectory) {
extent = new ExtentReports(outputDirectory + File.separator
+ "AutomationResult" + Instant.now().toEpochMilli() + ".html", true);
for (ISuite suite : suites) {
Map<String, ISuiteResult> result = suite.getResults();
for (ISuiteResult r : result.values()) {
ITestContext context = r.getTestContext();
buildTestNodes(context.getPassedTests(), LogStatus.PASS);
buildTestNodes(context.getFailedTests(), LogStatus.FAIL);
buildTestNodes(context.getSkippedTests(), LogStatus.SKIP);
}
}
extent.flush();
extent.close();
}
}
IReporter is a TestNG interface that fires once at the end of the suite. We iterate all results and write to an HTML report.
IReporter vs ITestListener โ important interview distinction¶
| Interface | Fires when | Use for |
|---|---|---|
IReporter |
Once at suite end with full results | Batch report generation |
ITestListener |
On every event (test start/pass/fail/skip) | Per-test screenshots, live logging |
ISuiteListener |
Suite start / finish | Suite-level setup |
IInvokedMethodListener |
Around every method (including @Before/After) | Method-level logging |
IAnnotationTransformer |
Once per class at startup, modifying annotations | Auto-applying retry analyzers |
Your Retry implements IAnnotationTransformer¶
public class Retry implements IAnnotationTransformer {
public void transform(ITestAnnotation testannotation, Class testClass,
Constructor testConstructor, Method testMethod) {
IRetryAnalyzer retry = testannotation.getRetryAnalyzer();
if (retry == null) {
testannotation.setRetryAnalyzer(RetryAnalyzer.class);
}
}
}
transform() for every @Test annotation during loading. We set RetryAnalyzer.class on any test that doesn't already have one. Result: every test in the suite gets retry-on-failure automatically โ no per-test opt-in.
The companion RetryAnalyzer¶
public class RetryAnalyzer implements IRetryAnalyzer {
int counter = 0;
int retryLimit = 1;
public boolean retry(ITestResult result) {
if (counter < retryLimit) { counter++; return true; }
return false;
}
}
Spoken answer โ "How does your retry mechanism work?"¶
"Two-class pattern.
RetryAnalyzerimplementsIRetryAnalyzerwith a counter and a retry limit of one. TheRetrylistener implementsIAnnotationTransformerโ TestNG calls itstransformmethod for every@Testannotation during class loading. We check if the test already has a retry analyzer; if not, we wire inRetryAnalyzer.class. Result is that every test in the suite gets one automatic retry on failure without devs having to opt in test-by-test. We register theRetrylistener in the suite XML once, and it covers all 200+ tests. The single retry is intentional โ anything that fails twice in a row is a real bug, not flakiness."
10.5 Suite XML structure (your orderAPI.xml)¶
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Avysh B2B Product Test_Suite" verbose="2"
parallel="classes" thread-count="2">
<parameter name="runParallel" value="true" />
<parameter name="environment" value="config.properties" />
<parameter name="browser" value="Chrome" />
<parameter name="hubURL" value="http://localhost:4444/wd/hub" />
<listeners>
<listener class-name="com.avysh.qa.extentreport.ExtentReporterNG" />
</listeners>
<test name="Avysh B2B Smoke Product Test">
<groups>
<run>
<include name="Order-APIs" />
</run>
</groups>
<classes>
<class name="com.avysh.qa.module.orders.OrdersAPITest"/>
</classes>
</test>
</suite>
| Element | Effect |
|---|---|
parallel="classes" |
Each class runs in its own thread |
parallel="methods" |
Each @Test method runs in its own thread |
parallel="tests" |
Each <test> tag runs in its own thread |
thread-count |
Concurrency cap |
<parameter> |
Available to @Parameters in test classes |
<groups><run><include> |
Filter by @Test(groups=...) |
11. PARALLEL EXECUTION + THREADLOCAL DRIVER¶
11.1 The ThreadLocal driver pattern¶
With parallel="classes" thread-count="4", TestNG runs four test classes simultaneously. Each needs its own browser. If you used a single static WebDriver driver, all four threads would fight for the same driver โ chaos.
The fix: ThreadLocal<WebDriver>.
public class WebDriverUtils {
public ThreadLocal<RemoteWebDriver> threadDriver = null;
public void setDriver(ThreadLocal<RemoteWebDriver> threadDriver) {
this.threadDriver = threadDriver;
}
public WebDriver getDriver() {
if (threadDriver == null) return driver; // fallback for non-parallel
return threadDriver.get(); // each thread sees its own
}
}
ThreadLocal<T> is a Java class โ each thread sees its own value via .get()/.set(). Different threads can't read each other's values. So per-thread driver isolation comes free.
Spoken answer โ "How does parallel execution work in your framework?"¶
"Two layers. At the suite level, TestNG's
parallel='classes' thread-count='4'runs each test class in its own thread, up to four concurrent. At the driver layer, WebDriverUtils holds aThreadLocal<RemoteWebDriver>โ when each test class callsgetDriver(), it gets a thread-local driver instance. So thread 1 has its Chrome window, thread 2 has its Chrome window, no cross-talk. The whole pattern works because TestNG creates a fresh thread per class, and ThreadLocal partitions state by thread identity. Without ThreadLocal, all four classes would write to the samedriverfield and overwrite each other's browser handles every few seconds."
11.2 Important โ don't forget to clean up ThreadLocal¶
After test class ends, call threadDriver.remove() โ otherwise the thread keeps a reference to the dead WebDriver and you leak memory in long CI runs.
@AfterClass(alwaysRun = true)
public void tearDown() {
driver.quit();
threadDriver.remove(); // <โ important
}
12. SELENIUM GRID¶
12.1 What it is¶
Distributed Selenium โ one hub routes test sessions to multiple nodes (machines or containers running a browser).
Test โ Hub (router)
โโ Node 1: Chrome on Mac
โโ Node 2: Firefox on Windows
โโ Node 3: Edge on Linux
12.2 Connecting from test code¶
RemoteWebDriver driver = new RemoteWebDriver(
new URL("http://hub:4444/wd/hub"),
new ChromeOptions()
);
Your WebDriverUtils.initializeDriver() takes a hubURL parameter โ when set, you route via Grid; when empty, you run locally.
12.3 Selenium 4 Grid modes¶
- Standalone โ single process, hub + node combined (for dev)
- Hub-node โ separate hub + nodes (classic)
- Distributed โ separate hub, session-queue, distributor, router (for scale)
- Docker โ official images, easy to compose
12.4 Docker compose example¶
services:
hub:
image: selenium/hub:4
ports: ["4444:4444"]
chrome:
image: selenium/node-chrome:4
shm_size: 2g
depends_on: [hub]
environment:
- SE_EVENT_BUS_HOST=hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
- SE_NODE_MAX_SESSIONS=5
firefox:
image: selenium/node-firefox:4
depends_on: [hub]
environment:
- SE_EVENT_BUS_HOST=hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
docker-compose up, then point your tests at http://localhost:4444/wd/hub.
13. SELENIUM 4 CDP โ NETWORK, GEOLOCATION, CONSOLE¶
This is the killer Selenium 4 feature. Chrome DevTools Protocol โ direct access to Chrome internals.
13.1 Setup¶
ChromeDriver chrome = (ChromeDriver) driver;
DevTools devTools = chrome.getDevTools();
devTools.createSession();
13.2 Network throttling¶
devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));
devTools.send(Network.emulateNetworkConditions(
false, // offline?
100, // latency ms
10_000, // download bytes/s (10 Mbps)
5_000, // upload bytes/s
Optional.empty()
));
13.3 Capture network requests¶
devTools.addListener(Network.requestWillBeSent(), req -> {
System.out.println(req.getRequest().getMethod() + " " + req.getRequest().getUrl());
});
devTools.addListener(Network.responseReceived(), resp -> {
if (resp.getResponse().getStatus() >= 400) {
System.out.println("BAD: " + resp.getResponse().getUrl());
}
});
13.4 Block requests (e.g., images for speed)¶
devTools.send(Network.setBlockedURLs(List.of("*.png", "*.jpg", "*.woff2")));
devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));
13.5 Override geolocation, timezone, user agent¶
devTools.send(Emulation.setGeolocationOverride(
Optional.of(12.97), Optional.of(77.59), Optional.of(1.0))); // Bangalore
devTools.send(Emulation.setTimezoneOverride("Asia/Kolkata"));
devTools.send(Network.setUserAgentOverride("MyTestUA/1.0", Optional.empty(),
Optional.empty(), Optional.empty()));
13.6 Capture console logs¶
devTools.send(Log.enable());
devTools.addListener(Log.entryAdded(), entry -> {
System.out.println("[" + entry.getLevel() + "] " + entry.getText());
});
Spoken answer โ "What's Selenium 4 CDP and have you used it?"¶
"CDP is the Chrome DevTools Protocol โ the same protocol Chrome's own DevTools uses. Selenium 4 exposes it directly via
devTools.send(...)anddevTools.addListener(...). Practical uses: throttle the network to simulate 3G, mock geolocation for location-sensitive tests, override user agent, capture every network request and assert no 4xx hits, block images and analytics requests to speed up tests, listen to browser console for JS errors. In the Avysh framework I didn't use it heavily โ we were on Selenium 4 alpha-5 from 2020 which had limited CDP support โ but for a fresh Selenium 4 framework today, it would be a major part of the testing strategy. Playwright has all of this built into its core API, which is one reason it's preferred for new work."
14. HEADLESS MODE + CI FLAGS¶
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new"); // Chrome 109+ new headless
options.addArguments("--no-sandbox"); // Docker as-root workaround
options.addArguments("--disable-dev-shm-usage"); // /dev/shm too small in Docker
options.addArguments("--disable-gpu"); // legacy Windows headless fix
options.addArguments("--window-size=1920,1080"); // viewport for screenshots
options.addArguments("--disable-extensions"); // faster startup
options.addArguments("--disable-blink-features=AutomationControlled"); // hide automation flag from bot-detection
WebDriver driver = new ChromeDriver(options);
When tests behave differently headless vs headed¶
- Different default viewport โ set
--window-sizeexplicitly - No GPU โ canvas / WebGL behaviour differs
- No font fallback to system โ install fonts in the Docker image
- Bot detection sometimes fires on headless โ the
--disable-blink-features=AutomationControlledflag helps
15. EXCEPTIONS โ WHAT EACH ONE MEANS¶
| Exception | Cause | Fix |
|---|---|---|
| NoSuchElementException | Locator didn't resolve | Verify locator; add explicit wait; check parent frame |
| StaleElementReferenceException | Reference held to detached DOM node | Re-find inside a fluent wait that ignores it |
| ElementNotInteractableException | Element exists but is hidden / disabled / covered | Wait for elementToBeClickable; check overlay |
| ElementClickInterceptedException | Another element is intercepting clicks | Scroll into view; close the intercepting element; JS click as last resort |
| TimeoutException | Explicit wait timed out | Increase timeout, fix condition, check selector |
| WebDriverException | Generic driver error | Check driver/browser version compatibility |
| NoSuchWindowException | switchTo().window() with invalid handle | Re-fetch handles |
| NoSuchFrameException | switchTo().frame() invalid index/name | Verify frame exists; switch to defaultContent first |
| NoAlertPresentException | switchTo().alert() with no alert | Wait for alertIsPresent first |
| InvalidSelectorException | XPath or CSS syntax error | Validate locator in DevTools |
| SessionNotCreatedException | Driver init failed | Driver / browser version mismatch; outdated chromedriver |
| UnreachableBrowserException | Browser died mid-test | Memory; OOMKilled in Docker; bumped Chrome version |
| JavascriptException | executeScript JS error |
Debug the JS โ wrap in try/catch |
Spoken answer โ "What's the most common Selenium exception and how do you handle it?"¶
"StaleElementReferenceException by a wide margin. It happens when I hold a WebElement reference, the page re-renders that part of the DOM, and the reference now points to a detached node. There are three fixes depending on the cause. First, if the re-render is predictable โ like after clicking 'Save' and the list reloads โ I re-fetch the element after the action. Second, for timing-driven re-renders, I wrap in a FluentWait that ignores StaleElement and retries the action. Third, for pages that constantly re-render โ like analytics dashboards with live data โ I move from WebElement references to By locators, so every access re-resolves. The CommonUtils.click in our framework does the second approach โ try, catch StaleElement, wait for clickable, retry."
16. REPORTING โ EXTENTREPORTS LISTENER PATTERN¶
16.1 Your ExtentReporterNG (real code, condensed)¶
public class ExtentReporterNG implements IReporter {
private ExtentReports extent;
public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites,
String outputDirectory) {
extent = new ExtentReports(outputDirectory + File.separator
+ "AutomationResult" + Instant.now().toEpochMilli() + ".html", true);
for (ISuite suite : suites) {
Map<String, ISuiteResult> result = suite.getResults();
for (ISuiteResult r : result.values()) {
ITestContext context = r.getTestContext();
buildTestNodes(context.getPassedTests(), LogStatus.PASS);
buildTestNodes(context.getFailedTests(), LogStatus.FAIL);
buildTestNodes(context.getSkippedTests(), LogStatus.SKIP);
}
}
extent.flush();
extent.close();
}
private void buildTestNodes(IResultMap tests, LogStatus status) {
if (tests.size() > 0) {
for (ITestResult result : tests.getAllResults()) {
ExtentTest test = extent.startTest(result.getMethod().getMethodName());
test.setStartedTime(getTime(result.getStartMillis()));
test.setEndedTime(getTime(result.getEndMillis()));
for (String group : result.getMethod().getGroups()) {
test.assignCategory(group);
}
if (result.getThrowable() != null) {
test.log(status, result.getThrowable());
} else {
test.log(status, "Test " + status.toString().toLowerCase() + "ed");
}
extent.endTest(test);
}
}
}
}
16.2 The two reporting patterns โ which to use when¶
| Pattern | When | Trade-offs |
|---|---|---|
IReporter (your pattern) |
At end of suite, batch-process all results | Misses real-time logging; no per-action screenshots |
ITestListener |
Hook every test event individually | Per-test screenshots, real-time logging, slightly more complex |
The modern shape is ITestListener because you can:
- Take a screenshot at the moment of failure (vs only end-of-test)
- Capture step-by-step logs inside the test
- Stream progress to a live dashboard
- Hook ExtentReports 5+ which uses ExtentTest per test
Honest tech-debt callout โ "Your ExtentReporter is the old IReporter pattern. What would you change?"¶
"Yes โ
IReporterbatch-processes at the end, which means I miss real-time progress and can only attach the exception, not screenshots at the failing step. The modern pattern isITestListenerplus ExtentReports 5.x. InonTestFailure, I'd capture a screenshot viaTakesScreenshoton the driver, attach it to the ExtentTest node, and log the throwable. I'd also add ExtentReports' built-inExtentSparkReporterfor the HTML output, which has dark mode, search, filter, and trend history. The library version we have iscom.relevantcodes2.41 from 2016 โ eight years stale. Migration tocom.aventstack5.x is one day of work."
17. TECH DEBT IN B2BPROJECTTEST โ HONEST AUDIT¶
This is the seniority signal. Call out your own debts before the interviewer finds them.
| Tech debt | Why it's a problem | What I'd do |
|---|---|---|
| Selenium 4 alpha-5 (from 2020) | Pre-release; missing features, fewer bug fixes | Bump to Selenium 4.20+ stable |
| ExtentReports 2.41.1 (com.relevantcodes, 2016) | Old API, no modern reporter (Spark), no dark mode | Migrate to ExtentReports 5.x + ExtentSparkReporter |
| Java 1.8 source target | Misses var, records, sealed classes, switch expressions |
Move to Java 17 LTS |
RestAssured.baseURI = uri; as static mutation (in OrdersAPI) |
Two parallel API tests overwrite each other's base URI | Use instance-scoped given().baseUri(uri).when()... |
Assertions inside API helpers (Assert.assertEquals(statusCode, 200) in OrdersAPI.getOrdersOnStatus) |
Makes helpers unreusable for negative tests | Helpers return Response; tests assert |
Driver binaries in resources/driver/{os}/ |
Manual per-OS distribution; brittle | Use Selenium Manager / WebDriverManager (already in pom.xml but unused) |
Thread.sleep in waitForPageToCompleteState |
Worst-case 20s for a healthy page; magic 2s post-delay | Replace with WebDriverWait polling document.readyState |
@FindBy(xpath="//*[@webix_tm_id='...']") everywhere |
XPath is slower than CSS for simple attribute checks | Use By.cssSelector("[webix_tm_id='...']") for these |
IReporter instead of ITestListener |
No per-test screenshots; can't capture progress live | Migrate to ITestListener |
WebDriverUtils.loadPropertyFile uses stack-trace inspection to get package name |
Fragile, surprising at distance | Pass package/path explicitly or use Spring-style config |
| All locators are XPath even when CSS would do | Performance and maintainability | Convert simple attribute selectors to CSS |
Hard-coded API key in OrdersAPI.getOrdersOnStatus: "517f73c431514a562da1af6851h8q6851" |
Secret in source | Move to env var or properties |
| Browser config split between properties and ChromeOptions in code | One change requires touching both | Centralize in a BrowserOptionsFactory |
The spoken answer¶
"Several. The biggest: Selenium 4 alpha-5 from 2020 โ I'd bump to 4.20 stable, which gives me Selenium Manager and proper CDP support. ExtentReports 2.41 from 2016 โ I'd migrate to 5.x and switch from
IReportertoITestListenerso I can attach per-test screenshots. Java 1.8 to Java 17 LTS forvar, records, switch expressions. In the OrdersAPI helper,RestAssured.baseURI = uriis a static mutation that breaks parallel tests โ instance-scopedgiven().baseUri(uri)fixes it. Same helper hasAssert.assertEquals(statusCode, 200)inside โ assertions in API helpers make them unreusable for negative tests; helpers should return responses, tests should assert. The driver binaries inresources/driver/{os}/are a 2020 pattern โ Selenium Manager handles this in one line now. And thewaitForPageToCompleteStateuses Thread.sleep retries โ I'd replace it with a proper WebDriverWait pollingdocument.readyState. None of these are urgent on a stable test suite, but they're the list for the next refactor sprint."
18. SELENIUM โ PLAYWRIGHT MIGRATION¶
18.1 The pitch I'd make to my team¶
"Three wins, one risk. Wins: auto-wait removes 80% of our
WebDriverWait+ExpectedConditionsboilerplate, which is where most of our flakiness lived. Parallel execution is built into the config โ no Selenium Grid to maintain. WebKit support means we catch Safari bugs without needing a Mac. Risk: it's a rewrite, not a port. The Locator API, action API, and the async model are different enough that we can't sed-replace through. Migration path: keep Selenium suite running during the bridge, port one user flow per week starting with the smoke suite, retire the Selenium equivalent once Playwright is green for two sprints. We learn by doing, not by reading docs."
18.2 The mapping cheat-sheet¶
| Selenium (B2BProjectTest pattern) | Playwright equivalent |
|---|---|
driver.findElement(By.xpath(...)) |
page.locator('xpath=...') or page.getByRole(...) |
WebDriverWait + ExpectedConditions |
await expect(loc).toBeVisible() (auto-retry) |
@FindBy Page Factory |
Lazy locator fields in POM |
ThreadLocal<RemoteWebDriver> |
Worker = own browser, automatic |
TestNG parallel="classes" |
fullyParallel: true |
RetryAnalyzer + IAnnotationTransformer |
retries: isCI ? 2 : 0 |
ExtentReporterNG IReporter |
Playwright HTML reporter (built-in) |
JavascriptExecutor.executeScript |
page.evaluate(() => {...}) |
Actions.dragAndDrop |
await page.dragAndDrop(src, target) |
| Selenium Grid | Workers + sharding (built-in) |
driver.switchTo().frame |
page.frameLocator('#iframe') |
driver.switchTo().alert() |
page.on('dialog', d => d.accept()) |
driver.manage().getCookies() |
await context.cookies() |
CDP via DevTools.send(...) |
page.route('**/api/*', handler) โ first-class |
19. INTERVIEW QUESTIONS โ 35+ Q&A¶
19.1 Architecture & basics¶
Q1. Walk me through Selenium's architecture.¶
Short: Test โ Client library โ W3C protocol over HTTP โ Browser driver โ Browser. Long:
"Five layers. My test code uses the Selenium client library, in our case the Java bindings. The client serializes commands into the W3C WebDriver protocol โ used to be the JSON Wire Protocol before Selenium 4 โ and sends them over HTTP to a local browser driver like chromedriver. The driver knows how to control its specific browser via native APIs โ Chrome's DevTools Protocol, Firefox's Marionette, Safari's WebDriver. The driver translates each command, executes it, and returns the result. Every command is one HTTP request, which is the architectural reason Selenium is slower than Playwright's persistent WebSocket โ over 200 commands per test, the HTTP overhead adds up to real seconds."
Q2. What changed in Selenium 4?¶
[See section 1.2 for the full answer]
Q3. Difference between findElement and findElements?¶
"
findElementreturns the first match and throwsNoSuchElementExceptionif none found.findElementsreturns aList<WebElement>โ empty list if no match, no exception. UsefindElementwhen the element must exist;findElementswhen you're counting, iterating, or checking absence."
19.2 Waits¶
Q4. Walk me through the three waits and when to use each.¶
[See section 5.4 for the full answer]
Q5. What happens if you mix implicit and explicit wait?¶
"Behavior becomes implementation-specific. Selenium can wait for the maximum of the two, or behave unpredictably. The safe rule: set implicit wait to zero, use explicit waits everywhere. I enforce this in the driver setup."
Q6. What's the difference between visibilityOf and visibilityOfElementLocated?¶
"
visibilityOf(elem)takes an existingWebElementโ useful when you already have the reference. If the underlying DOM node is replaced, this wait throws StaleElement and you have to refetch.visibilityOfElementLocated(by)takes a locator and re-resolves it on each poll, so it survives DOM updates. Generally prefer the locator version for re-renders, the element version when you know the element is stable."
19.3 Locators¶
Q7. Difference between absolute and relative XPath?¶
"Absolute XPath starts at root โ
/html/body/div[1]/form/inputโ and breaks on the first DOM change. Relative XPath starts with//and matches anywhere in the tree โ//input[@id='email']. I never write absolute XPaths in production. Relative XPaths with attribute filters are stable across most layout changes."
Q8. When do you use XPath vs CSS?¶
[See section 4.3 for the full answer]
Q9. What are Selenium 4 Relative Locators?¶
"A new locator API in Selenium 4 โ
with(By.tagName('input')).toRightOf(label). The relations areabove,below,toLeftOf,toRightOf,near. It works by checking actual rendered position of elements. Useful when there's no formal label-input association in the DOM. Trade-off: it's a render-position check, so it's slower and can break on layout reflows. I'd use it for label-input forms where the existing locators are fragile, not as a default."
Q10. What is PageFactory and how does @FindBy work?¶
[See section 9.2 + 4.5 for the full answer]
19.4 Actions¶
Q11. How would you hover over an element and then click a submenu?¶
"Actions class.
new Actions(driver).moveToElement(menuRoot).perform();thensubmenuItem.click();. Sometimes the hover-then-click needs to be chained:actions.moveToElement(menuRoot).pause(500).click(submenuItem).perform();. The pause gives time for the CSS hover transition to complete."
Q12. How do you handle a file upload?¶
"If the page uses a standard
<input type='file'>, justsendKeysthe absolute file path:driver.findElement(By.id('upload')).sendKeys('/path/to/file.pdf'). This works because WebDriver writes to the input's value directly, bypassing the native OS file dialog. If the app uses a custom drag-drop uploader without a real input, you have to use JS โ read the file in JavaScript, create a File object, dispatch adropevent."
Q13. What's the difference between clear() and sendKeys(Keys.CONTROL + 'a') followed by Keys.DELETE?¶
"
clear()calls the DOMclearmethod, which sets value to empty. Some modern apps with controlled React inputs don't react to this because noinputevent fires. The Ctrl+A + Delete pattern simulates real keyboard input, which fireskeydown,keyup, andinputevents, so React re-validates. I default toclear()but fall back to the keyboard pattern when forms don't react."
19.5 JS executor¶
Q14. When would you use JavascriptExecutor?¶
[See section 7.2 for the full answer]
Q15. How do you scroll an element into the visible viewport?¶
"JS executor:
js.executeScript("arguments[0].scrollIntoView({block: 'center'});", element). Theblock: 'center'keeps it from being hidden by sticky headers. Selenium's built-in scrolling sometimes lands the element flush against a header, leading to ElementClickIntercepted."
19.6 Frames, windows, cookies¶
Q16. How do you handle multiple browser windows?¶
"Old way:
getWindowHandles(), iterate, switch to the one that's not the original. Selenium 4:driver.switchTo().newWindow(WindowType.TAB)opens a new tab and switches automatically. To go back, save the original handle before opening anything new andswitchTo().window(original)."
Q17. How do you persist login across tests in Selenium?¶
"Two approaches. The light approach: save cookies after login, reuse them in subsequent tests via
driver.manage().addCookie. Need to be on the same domain first. The deeper approach: use Chrome's--user-data-dir=/path/to/profileflag to point at a persistent profile โ login stays in the profile. The cookie approach is closer to what Playwright'sstorageStatedoes."
19.7 POM and frameworks¶
Q18. Walk me through your Page Object Model.¶
[See section 9.3 for the full answer โ Page + Helper + Test]
Q19. POM vs Page Factory?¶
"POM is the design pattern โ encapsulate page elements and actions in a class. Page Factory is Selenium's implementation helper that uses
@FindByannotations and reflection. Page Factory's value is locators sit next to fields and elements re-resolve lazily โ so no stale element issues. The trade-off is that errors come at use-time instead of construction-time, which can be confusing when debugging."
Q20. How do you organize 40+ pages?¶
"By feature module. In our B2BProjectTest, pages live under
com.avysh.qa.pages.*and tests undercom.avysh.qa.module.{regions, products, orders, manageteam, ...}.*. The Helper layer sits between โ pages own DOM, helpers own user workflows, tests orchestrate helpers. Suite XMLs split by module โ Smoke, brandPIM, sellerPIM, orderAPI, integration โ so CI can run only what's relevant per change."
19.8 TestNG¶
Q21. Order of TestNG annotations.¶
"BeforeSuite, BeforeTest, BeforeClass, BeforeMethod, @Test, AfterMethod, AfterClass, AfterTest, AfterSuite. Suite-level annotations fire once for the whole run. Test-tag annotations fire per
<test>in the suite XML. Class-level fire per test class. Method-level fire per@Test."
Q22. Difference between dependsOnMethods and priority?¶
"
priorityis a number โ lower runs first, but it's a hint not an order guarantee, especially in parallel.dependsOnMethodsis a hard dependency โ if the method this depends on fails, the dependent test is skipped, not failed. I avoiddependsOnMethodsfor normal flow; it's for cases like 'logout test only runs if login passed' where the dependency is functional, not convenience ordering."
Q23. Soft Assert vs Hard Assert?¶
"Hard assert โ
Assert.assertEquals(actual, expected)โ stops the test on first failure. Soft assert from AssertJ (which our framework uses) โsoftAssertions.assertThat(actual).isEqualTo(expected)โ collects failures and reports them all when you callsoftAssertions.assertAll(). WithoutassertAll(), failures silently pass. I use soft for validating multiple fields on an object โ like asserting 12 fields of an order response โ so I see all mismatches in one run. Hard for invariants where any failure should abort."
Q24. How does your retry mechanism work?¶
[See section 10.4 for the full answer]
Q25. What's the difference between IReporter and ITestListener?¶
[See section 10.4 + section 16.2 for the comparison]
19.9 Parallel¶
Q26. How does parallel execution work in your framework?¶
[See section 11.1 for the full answer]
Q27. What's a ThreadLocal and why is it needed?¶
"ThreadLocal is a Java class โ each thread sees its own value via
.get()/.set(), separate from other threads. In Selenium parallel execution, every test class runs in its own thread but uses the sameWebDriverUtils.getDriver()call. Without ThreadLocal, all threads would share a singledriverfield, so thread 1's click could land on thread 2's window. WithThreadLocal<WebDriver>, each thread gets its own driver reference, perfect isolation."
Q28. What's the difference between parallel='classes' and parallel='methods' in testng.xml?¶
"
parallel='classes'โ each test class runs in its own thread, methods within a class run serially.parallel='methods'โ each@Testmethod runs in its own thread regardless of class. Classes is safer when tests in the same class share state via@BeforeMethodsetup. Methods gives more parallelism but requires every@Testto be fully self-contained โ no shared instance state, fresh driver per method. We use classes in our framework because our@BeforeClassdoes the login and we want that login shared across that class's tests."
19.10 Grid¶
Q29. What is Selenium Grid and when would you use it?¶
"Distributed Selenium. A hub routes test sessions to one or more nodes, each running a specific browser/OS combination. Three real use cases. First, run tests across multiple browser-OS matrices in parallel for cross-browser coverage. Second, scale beyond a single machine โ 100 parallel tests need 100 browser processes, which takes more RAM than one box has. Third, dedicated machines for fragile tests โ Safari only runs on Mac, IE only on Windows. With Playwright's built-in workers we don't need Grid for single-machine scaling, but for matrix testing across OS-browser combinations, Grid is still useful."
Q30. How would you set up Selenium Grid in Docker?¶
"Use the official
selenium/hub:4andselenium/node-chrome:4,selenium/node-firefox:4images. Docker Compose them โ hub on port 4444, nodes pointing at the hub viaSE_EVENT_BUS_HOST. Setshm_size: 2gon the Chrome node โ Chrome crashes with 64MB shm. SetSE_NODE_MAX_SESSIONSto control concurrency per node. Then point yourRemoteWebDriverathttp://hub:4444/wd/hub. The new mode in Selenium 4 also supports auto-scaling via Docker, which means new nodes spin up on demand."
19.11 Exceptions + debugging¶
Q31. What's the most common Selenium exception and how do you handle it?¶
[See section 15 for the full answer]
Q32. How do you debug a flaky Selenium test?¶
"Six-step process I follow. First, run it 50 times locally โ if it fails even once, it's flaky. Second, look at the failure mode โ what exception, at what line. Third, examine waits โ is it Thread.sleep or implicit wait? Replace with explicit wait on the specific condition. Fourth, check for shared state โ does this test depend on data left over from another test? Make it create its own data. Fifth, run in CI conditions โ headless, same parallelism โ some failures only reproduce in headless. Sixth, if nothing reproduces, add screenshot capture at the failing step via TestListener
onTestFailureso the next failure has evidence. Last resort, take a video of the run with FFmpeg or a Selenium 4 BiDi recording."
Q33. How do you take a screenshot on test failure?¶
"Implement
ITestListener.onTestFailure(ITestResult result). Inside, cast the driver:File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);then copy via FileUtils. Attach the screenshot path to the ExtentReports test node. We need to fetch the driver from the test context โ(WebDriver) result.getTestContext().getAttribute('driver')โ which means the test class must set it viacontext.setAttribute('driver', driver)in@BeforeMethod."
19.12 Practical patterns¶
Q34. How do you handle dynamic content that loads slowly?¶
"Layered. First, explicit wait on a stable element that signals 'load complete' โ usually a non-spinner like the first data row or a success indicator. Second, if the load is API-driven, wait for the API response via Selenium 4 CDP
Network.responseReceivedlistener โ explicit, not magic timeouts. Third, for Angular apps, the NgWebDriver pattern in our framework โngDriver.waitForAngularRequestsToFinish()โ hooks into Angular's zone to detect when all pending HTTP requests finish. I never use Thread.sleep as the primary wait."
Q35. How do you handle a captcha?¶
"Three approaches in order of preference. First, ask backend devs for a test-mode bypass โ most apps support a 'skip captcha for whitelisted test accounts' mode in dev/stage. Second, use captcha-bypass tokens supplied by the dev team โ pass a known-valid token in the request. Third, only as last resort, mock the captcha endpoint via Selenium 4 CDP
Fetch.requestPausedlistener and respond 'valid' to it. Never try to programmatically solve captchas โ they're designed to defeat automation."
Q36. Tell me about a real bug you found with Selenium.¶
"On the Avysh B2B app, we had a flow where the Order Status dropdown would silently not update if the user clicked too quickly after the page loaded. UI showed the old value, backend stored the new one โ so users would think nothing happened, click again, double-save. The Selenium tests caught it because our explicit waits had a timing margin โ our tests would set status to 'Fulfilled', then immediately read the displayed status, and the assertion would fail intermittently. Root cause was a missing
awaitin the Angular event handler. The dev fixed it; I added a regression test that explicitly clicks-then-reads-immediately to keep it from coming back."
Q37. How would you build a Selenium framework from scratch tomorrow?¶
"Same shape as B2BProjectTest but with the modernizations. Java 17, Selenium 4.20 stable, TestNG 7. Selenium Manager instead of per-OS driver binaries. ExtentReports 5 with
ITestListenerinstead of 2.41 withIReporter. Page Factory POM with Helper facade layer for business workflows. RestAssured for backend API tests integrated into the same suite. TestNG parallel='classes' with ThreadLocal driver. Docker-based Selenium Grid for cross-browser. CI: GitHub Actions matrix per browser. CDP for network throttling and request interception. The big change from B2BProjectTest: I'd default to CSS selectors over XPath, instance-scoped REST Assured, no static mutations, no assertions inside helpers, no Thread.sleep anywhere."
20. CAPACITY PLANNING & SCALE โ SCENARIO QUESTIONS¶
These are the "system-design for SDET" questions that separate a mid from a senior. They're rarely about a single API โ they test whether you can reason about throughput, infrastructure, cost, and where to push back on the requirement. Panels in 2026 increasingly ask "how would you run 10,000 tests a day" style questions, so keep the formula and the challenge-the-premise move ready.
20.1 The master formula (memorize this)¶
Every "run N tests in T minutes" question reduces to one equation:
That's just Little's Law for tests: throughput you need = total work รท time budget. Everything else โ Grid, Docker, cloud, sharding โ is how you buy that concurrency. Say the formula out loud first; it signals you think in throughput, not tools.
The four levers (once you have N, every optimization is one of these):
1. โ Concurrency โ more parallel sessions (Grid nodes, cloud slots, CI shards).
2. โ Avg test time โ faster tests (API login, seeded data, headless, no Thread.sleep).
3. โ Test count โ push logic down the pyramid to API/unit; run only affected tests.
4. โ Browser matrix โ full run on one browser, targeted subset on the rest.
Q38. "100 tests, 3 browsers (Chrome, Safari, Firefox), must finish under 1 minute (~10s per test). Plan it." โญ¶
This is the flagship scenario. Answer in five moves.
Move 1 โ Clarify assumptions out loud¶
The answer swings entirely on these, so state them back before you compute (they've handed us ~10s/test): - Average test duration โ given as ~10s. (If it weren't, I'd ask โ 5s vs 20s doubles the infra.) - Are the tests independent / parallel-safe? (own data, no shared static state) - Do all 100 genuinely need all 3 browsers? (almost never โ Move 4) - Self-hosted or cloud grid budget?
Move 2 โ Do the math¶
100 tests ร 3 browsers = 300 executions. At 10s each, serial = 3000s = 50 minutes. Target = 60s:
Add ~20% headroom for setup/teardown, queueing, stragglers โ plan for ~60 (~17 per browser).
Move 3 โ The Safari constraint (this is the differentiator โ lead with it)¶
- Safari only runs on macOS. No Linux, no
selenium/node-safariDocker image โ you can't containerize it like Chrome/Firefox. safaridriverallows exactly ONE session per machine. Hard Apple limit, not a config knob. You physically cannot run 2 concurrent Safari sessions on one Mac.
So the concurrency math that works for Chrome collapses for Safari (each browser needs ~17 concurrent):
| Browser | Runs on | ~17 concurrent sessions means | Feasible? |
|---|---|---|---|
| Chrome | Linux/Docker nodes | ~1โ2 ร 16-vCPU containers | โ Easy, autoscale |
| Firefox | Linux/Docker nodes | ~1โ2 ร 16-vCPU containers | โ Easy |
| Safari | macOS only, 1 session/Mac | ~17 physical Macs | โ Absurd to self-host |
Even at this small scale, Safari is the wall: 17 idle Macs sitting in a rack for a 1-minute job makes no sense.
Move 4 โ The senior move: challenge "3 browsers ร 100"¶
Cross-browser risk is mostly rendering/CSS/WebKit quirks, not business logic โ and business logic is already fully covered by the 100 on Chrome. So: - Run the full 100 on Chrome (primary) + full 100 on Firefox โ both cheap on Linux, ~17 sessions each. - Run only a ~20-test rendering-critical subset on Safari via a cloud Mac farm (BrowserStack/Sauce/LambdaTest โ they own the Macs; you rent parallel slots). - Safari concurrency drops: 20 ร 10 / 60 = ~4 cloud Mac sessions โ trivial and cheap.
Revised totals:
| Browser | Tests | Concurrency | Infra |
|---|---|---|---|
| Chrome | 100 | ~17 | Self-hosted Grid/Selenoid (1โ2 nodes) |
| Firefox | 100 | ~17 | Self-hosted Grid/Selenoid (1โ2 nodes) |
| Safari | ~20 (critical) | ~4 | Cloud Mac farm |
| Total | ~38 concurrent | only ~4 are the expensive Macs |
Move 5 โ Make it real¶
- Infra: at this scale a small Grid/Selenoid fleet โ 2โ3 Docker nodes for Chrome+Firefox โ is plenty; no need for heavy autoscaling. Buy the ~4 Safari slots from a cloud grid.
- Parallel-safe tests:
ThreadLocal<WebDriver>,parallel="methods", no static mutation (RestAssured.baseURI = ...breaks under load), each test seeds its own data. - Shard across CI runners if you want it even faster โ e.g. a few GitHub Actions runners splitting the 100,
--shard i/n. - Shrink per-test time โ inject session via
storageState/cookie instead of UI login; seed data via API; headless with the CI flag quartet. (Getting 10s โ 6s would let you hit the target with even fewer sessions.)
Spoken answer¶
"First the formula: 100 tests ร 3 browsers is 300 runs; at ~10s each that's 50 minutes serial, so to hit 1 minute I need concurrency of 300ร10/60 = 50 parallel sessions, plus headroom, call it 60 โ about 17 per browser. Chrome and Firefox I'd run the full 100 each on a small Grid or Selenoid fleet โ one or two Docker nodes each, easy on Linux. But the moment Safari's in the mix the plan changes: Safari only runs on macOS and
safaridriverallows one session per machine, so 17 parallel Safari would mean 17 physical Macs for a one-minute job. I wouldn't do that. Instead I'd run only the ~20 rendering-critical tests on Safari via a cloud Mac farm โ about 4 parallel sessions โ because Safari bugs are WebKit rendering quirks, not business logic, and the logic is already covered on Chrome. That lands the whole thing under a minute at ~38 total concurrency, only 4 of which are the expensive Macs. Tests have to be parallel-safe โ ThreadLocal drivers, no static state, own data."
Variant A โ the 3 browsers are Chromium, Firefox, Edge (all Linux-capable) โ the "happy path"¶
The Safari wall disappears: Chromium, Firefox, and Edge all run headless on Linux and all have official Docker node images (selenium/node-chromium, selenium/node-firefox, selenium/node-edge). No macOS, no cloud Mac farm, no one-session-per-machine limit โ the matrix is fully symmetric and entirely self-hosted.
The math is identical: 300 runs โ 50 concurrent (~17/browser) โ ~3โ4 ร 16-vCPU Docker nodes behind one Grid. Point RemoteWebDriver at the hub, parallel="methods" + ThreadLocal, done in ~1 minute.
The clever bit here isn't an infra limit โ it's engine redundancy. Chromium and Edge are both Blink (Edge has been Chromium-based since 2020); Firefox is Gecko โ the only distinct engine in this set. So running the full 100 on both Chromium and Edge is near-redundant at the rendering level (Edge's real differences are IE-mode/SmartScreen/enterprise-policy, not layout). If the matrix were large I'd run full on Chromium + Firefox and only a ~20-test Edge-specific subset on Edge (300 โ ~220 runs, ~37 concurrency). At 100 tests in a minute it's cheap enough to just run all three fully โ optimize the matrix only when scale demands it.
Variant B โ the 3 browsers use 3 different engines โ how the calc changes¶
The formula never changes โ N = (tests ร browsers ร avg_time) / target is engine-agnostic. What engines change is how much of the matrix you're allowed to trim โ the effective browser multiplier.
There are only three engines in the world: Blink (Chrome/Chromium/Edge/Opera/Brave), Gecko (Firefox), WebKit (Safari). So "3 different engines" = Chrome + Firefox + Safari, and:
No rendering redundancy โ you cannot shrink the matrix on engine grounds โ the multiplier stays at full 3.
3 distinct engines (Chrome+Firefox+Safari): every browser is a unique signal
โ run full 100 on all 3 โ 300 executions โ N = 50 (~17/browser)
Shared engine (Chromium+Edge+Firefox = Blink+Blink+Gecko): one is redundant
โ full 100 + full 100 + ~20 subset โ ~220 executions โ N โ 37
The sting: if the third engine is WebKit, that's Safari โ the macOS / one-session-per-machine / cloud-Mac-farm wall returns for that browser. So 3 distinct engines is the case where you can't trim and the one that reintroduces the Safari infra problem.
One-liner: "Count engines, not browsers. Same-engine browsers are near-redundant, so I trim the extra to a subset; distinct engines each carry unique rendering risk, so I run them fully โ and if one of those engines is WebKit, Safari's macOS constraint drives the infra."
Q39. "Your regression suite takes 4 hours. Get it under 30 minutes." (reduce execution time)¶
"8ร speedup โ I'd attack it on four fronts, biggest ROI first. One, parallelize: if it's serial today,
parallel='methods'+ ThreadLocal + a Grid/Selenoid fleet gives near-linear speedup up to my node count โ that alone is often 4โ6ร. Two, cut per-test time: most Selenium suites bleed time on UI login andThread.sleepโ inject the session via cookie/storageState, seed data via API, replace every sleep with explicit waits, run headless. Three, right-size the pyramid: a 4-hour UI suite usually means logic that belongs in fast API/unit tests โ I'd move contract checks down to RestAssured and keep UI for true journeys. Four, run only what changed: test-impact analysis / selection so a PR runs the affected slice, and the full suite runs nightly. I'd measure first โ find the slowest 20% of tests, they're usually 80% of the wall-clock."
Q40. "Design test infrastructure to run 10,000 tests a day across a team." (system design)¶
"I'd think in four layers. Execution grid: autoscaling Selenium Grid or Selenoid/Moon on Kubernetes, nodes spun on demand from the session queue so we pay only for what runs; cloud farm for Safari/real devices. Test-data layer: each run provisions isolated data โ either per-worker namespaces or an API that mints fresh accounts โ so 100 parallel workers never collide; this is the #1 thing that breaks at scale. Orchestration: CI (GitHub Actions/Jenkins) shards the suite across runners, triggers on PR + nightly, with test-selection so PRs run fast. Observability: central reporting (ExtentReports/Allure), per-test video/trace on failure, flaky-test tracking with quarantine, and dashboards for pass-rate and duration trends. The themes interviewers want: isolation, elasticity, and failure debuggability in a distributed system."
Q41. "How do you generate/isolate test data across hundreds of parallel workers?"¶
"Never share mutable data across parallel tests โ that's the classic flaky-at-scale trap. Three patterns, best first. Data factories / API seeding: each test creates exactly what it needs via an API call in setup and tears it down after โ fully isolated, no collisions. Per-worker namespacing: prefix data with the worker/thread ID (
user_${workerId}@test.com) so workers can't touch each other's rows. Dedicated pools: a pool of pre-created accounts leased per test and returned. I avoid a shared golden dataset โ the moment two parallel tests mutate the same record you get non-deterministic failures that only appear under load."
Q42. "How would you set up a dynamic/auto-scaling Grid so you're not paying for idle nodes?"¶
"Selenium 4 Grid on Kubernetes with the queue-driven autoscaler, or Selenoid/Moon which spin one container per session and destroy it after. Scale signal is session-queue depth โ KEDA watches the queue and adds Chrome/Firefox pods when tests are waiting, scales to zero when idle. Set
SE_NODE_MAX_SESSIONSto ~1 per vCPU andshm_size: 2gon Chrome nodes. For Safari/real devices I don't self-host โ I burst to a cloud farm and pay per parallel slot. The point is elasticity: full fleet during the nightly regression, near-zero cost at 3am when nothing's running."
Q43. "How much parallelism is 'safe' on one machine? How do you size a node?"¶
"Rule of thumb: ~1 browser session per vCPU and ~1โ1.5 GB RAM per Chrome. So a 16-vCPU / 24 GB node runs ~16 Chrome sessions comfortably โ push past that and context-switching and OOM kills make tests slower and flakier, not faster. I tune
SE_NODE_MAX_SESSIONSto the vCPU count, watch forUnreachableBrowserException/OOMKilled as the signal I've overcommitted, and scale out (more nodes) rather than up (more sessions per node) past that ceiling. Chrome needsshm_size: 2gor it crashes regardless of RAM."
Q44. "Cross-browser matrix is exploding your CI time โ how do you keep coverage without running everything everywhere?"¶
"I don't run the full suite on every browser โ that's quadratic waste. Full logic coverage on one primary browser (Chrome), then a curated cross-browser subset on the others โ the rendering-sensitive and browser-quirk-prone flows only. Cross-browser bugs are overwhelmingly CSS/layout/WebKit issues, so the subset targets exactly those. In CI I model it as a
matrixbut with different test tags per browser:@smoke @cross-browserruns everywhere, the full regression only on Chrome. Cuts matrix cost 60โ70% with negligible risk to real coverage."
QUICK CHEAT SHEET¶
Most-used commands¶
mvn clean test # all tests
mvn test -Dtest=RegionsTest # one class
mvn test -Dgroups=Smoke_Test # by group
mvn test -DsuiteXmlFile=Smoke_Testng.xml # specific suite
mvn test -Dbrowser=chrome -Dmode=headless # via properties
Five lines that signal seniority in interview¶
- "Don't mix implicit and explicit waits โ set implicit to zero and use explicit everywhere."
- "ThreadLocal is what makes parallel TestNG work without driver collisions."
- "Page Object Model is the pattern, Page Factory is Selenium's implementation โ the lazy proxy is the win."
- "
@AfterMethod onTestFailureplusITestListenerfor per-test screenshots โIReporteris end-of-suite only." - "
RestAssured.baseURI = uriis a static mutation that breaks parallel API tests โ usegiven().baseUri(uri).when()instead." - "Any 'run N tests in T minutes' question is just
concurrency = (tests ร browsers ร avg_time) / targetโ then buy that concurrency and challenge the browser matrix." - "Safari can't be containerized โ macOS-only, one session per machine โ so you burst it to a cloud Mac farm and run only the rendering-critical subset there."
Owner: Rohan Dsouza | Grounded in: Avysh B2BProjectTest (Java + Selenium 4 + TestNG + REST Assured + ExtentReports) | Updated: 2026