Sitemap

5 Selenium Anti-Patterns Found in Production Frameworks — And How to Fix Them

6 min readJun 26, 2026
Press enter or click to view image in full size

These patterns look harmless at first. At scale, they’ll destroy your test suite.

Every Selenium framework starts clean.

Then the suite grows. Deadlines hit. Quick fixes get committed. Before long, tests are flaky, CI runs take forever, and nobody wants to touch the framework.

Most of the time, the root cause isn’t bad intentions . it’s a handful of anti-patterns that sneak in early and compound over time.

Here are five of the most common ones found in production Selenium frameworks, why they’re dangerous, and exactly how to fix them.

Anti-Pattern 1: Static WebDriver

The Problem

// ❌ Anti-pattern — static WebDriver
public class TestBase {
protected static WebDriver driver;
protected static WebDriverWait wait;
    public void initialization() {
driver = new ChromeDriver();
wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
}

Static WebDriver looks harmless in a small suite. But the moment you try to run tests in parallel, everything breaks.

Why? Because static variables are shared across all threads. When two tests run simultaneously, they’re both writing to the same driver variable overwriting each other, causing random failures, and producing results that are impossible to debug.

This is one of the most common reasons parallel execution fails silently in large Selenium suites.

The Fix — ThreadLocal WebDriver

// ✅ Industry standard — ThreadLocal WebDriver
public class DriverFactory {
private static final ThreadLocal<WebDriver> driver = new ThreadLocal<>();
    public static WebDriver getDriver() {
return driver.get();
}
public static void setDriver(WebDriver webDriver) {
driver.set(webDriver);
}
public static void quitDriver() {
if (driver.get() != null) {
driver.get().quit();
driver.remove(); // Important: prevent memory leaks
}
}
}

ThreadLocal gives each thread its own isolated WebDriver instance. Thread A's driver never touches Thread B's driver. Parallel execution becomes safe, predictable, and fast.

Key rule: Always call driver.remove() after quitting , failing to do so causes memory leaks in long-running CI jobs.

Anti-Pattern 2: Implicit Wait + Thread.sleep()

The Problem

// ❌ Anti-pattern — global implicit wait
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(15));
// ❌ Anti-pattern — Thread.sleep
public void selectFromDropdown(String option) {
dropdown.click();
Thread.sleep(3000); // "just wait for it to open"
option.click();
}

There are two problems here:

Implicit wait applies globally to every findElement call. Mix it with explicit WebDriverWait and you get unpredictable behaviour the Selenium docs explicitly warn that combining both can cause timeouts to add together unexpectedly.

Thread.sleep is worse. It’s a fixed pause regardless of what the app is doing. If the element appears in 500ms, your test still waits 3 seconds. If the app is slow and takes 4 seconds, your test fails. It’s the worst of both worlds slow AND brittle.

The Fix — Explicit Waits Only

// ✅ Industry standard — explicit waits only
// Set implicit wait to zero
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(0));
// Wait for exactly what you need
public void selectFromDropdown(By dropdownLocator, By optionLocator) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.elementToBeClickable(dropdownLocator)).click();
wait.until(ExpectedConditions.visibilityOfElementLocated(optionLocator)).click();
}

Explicit waits poll for a specific condition element visible, clickable, text present and proceed the moment that condition is true. No unnecessary waiting. No surprise timeouts.

Rule of thumb: Zero implicit wait. No Thread.sleep. Explicit WebDriverWait everywhere.

Anti-Pattern 3: Swallowing Exceptions in Wrapper Methods

The Problem

// ❌ Anti-pattern — swallowing exceptions
public KeywordBase click(By locator) {
try {
WebElement element = wait.until(
ExpectedConditions.elementToBeClickable(locator)
);
element.click();
} catch (Exception e) {
logger.error("Failed to click element: {}", locator, e);
// continues silently...
}
return this;
}

This pattern feels defensive. Log the error, keep going what could go wrong?

Everything.

When your click() fails silently, the next step in the test runs against a UI that's in the wrong state. Sometimes the test still passes. You now have a test that passes even when the feature is broken.

Silent failures are harder to debug than loud ones. A test that fails clearly tells you exactly what went wrong. A test that silently continues gives you a false green result and a production bug.

The Fix — Fail Fast

// ✅ Industry standard — fail fast
public WebElement click(By locator) {
try {
WebElement element = wait.until(
ExpectedConditions.elementToBeClickable(locator)
);
element.click();
return element;
} catch (TimeoutException e) {
throw new RuntimeException(
"Element not clickable after timeout: " + locator, e
);
} catch (ElementClickInterceptedException e) {
throw new RuntimeException(
"Click intercepted — possible overlay blocking: " + locator, e
);
}
}

Throw meaningful exceptions. Name the locator. Specify what went wrong. Your CI pipeline will catch it immediately, and your error message will tell the next person exactly where to look.

Rule: Never catch Exception broadly in action methods. Catch specific exceptions and rethrow with context.

Anti-Pattern 4: XPath-Heavy Locator Strategy

The Problem

// ❌ Anti-pattern — brittle XPath locators
By emailField = By.xpath("//input[@placeholder='Email or mobile']");
By submitButton = By.xpath("//button[@type='submit']");
By menuItem = By.xpath("//div[contains(@class,'css-1n7v3ny-option') and text()='Settings']");

XPath locators tied to placeholder text, button types, or CSS class names break constantly:

  • Designer changes placeholder copy → locator breaks
  • Frontend team refactors CSS class names → locator breaks
  • Copy team updates button label → locator breaks

The third example is the worst — css-1n7v3ny-option is a hash generated by a CSS-in-JS library. It changes on every build.

The Fix — Stable Locator Priority

Work with your development team to add data-testid attributes to key elements. This is the highest-ROI conversation a QE can have with a dev team.

<!-- Dev adds this to the component -->
<input data-testid="login-email-field" placeholder="Email or mobile" />
<button data-testid="login-submit-btn" type="submit">Sign In</button>
// ✅ Industry standard — stable locators
By emailField = By.cssSelector("[data-testid='login-email-field']");
By submitButton = By.cssSelector("[data-testid='login-submit-btn']");
// ARIA role locators — also stable and accessibility-friendly
By submitButton = By.xpath("//button[@aria-label='Sign In']");

Locator priority in 2026:

1. data-testid          ← most stable, explicitly for testing
2. ARIA role / label ← stable, accessibility-aligned
3. Stable CSS selector ← ID, name attributes
4. XPath ← last resort only

data-testid attributes are ignored by production users and never changed by designers they exist purely for your test suite. Once your team adopts them, locator maintenance drops dramatically.

Anti-Pattern 5: God Classes

The Problem

// ❌ Anti-pattern — one giant page class
public class CustomersPage extends LoginPage {
// 6,000+ lines
// 200+ locators
// Methods for search, orders, invoices, settings, profiles...
// Inherits login behavior it doesn't always need
}

God classes massive page objects that cover everything are the natural endpoint of a framework that grows without refactoring. They’re hard to navigate, slow to load in IDEs, and become a merge conflict nightmare in teams.

Inheritance-based page objects (CustomersPage extends LoginPage) compound this you couple unrelated concerns together and create fragile dependency chains.

The Fix — Component Model

Break large page objects into focused, reusable components:

// ✅ Industry standard — component model
// Small, focused components
public class SearchComponent {
private final WebDriver driver;
private By searchInput = By.cssSelector("[data-testid='search-input']");
private By searchButton = By.cssSelector("[data-testid='search-btn']");
public SearchComponent(WebDriver driver) {
this.driver = driver;
}
public void searchFor(String query) {
driver.findElement(searchInput).sendKeys(query);
driver.findElement(searchButton).click();
}
}
public class OrderGuideComponent {
// Only order guide locators and methods
}
// Page class composes components — no inheritance
public class CustomersPage {
public final SearchComponent search;
public final OrderGuideComponent orderGuide;
public CustomersPage(WebDriver driver) {
this.search = new SearchComponent(driver);
this.orderGuide = new OrderGuideComponent(driver);
}
}

Now your test reads like this:

customersPage.search.searchFor("customer-001");
customersPage.orderGuide.openForCustomer("customer-001");

Each component is small, focused, and independently testable. Changes to the search bar touch SearchComponent only not a 6,000-line monster class.

Summary

Anti-Pattern Risk Fix Static WebDriver Parallel execution breaks ThreadLocal per thread Implicit wait + sleep Flaky, slow tests Explicit waits only Swallowed exceptions Silent false positives Fail fast, rethrow with context XPath-heavy locators Constant breakage data-testid + ARIA roles God classes Unmaintainable codebase Component model

A Note on Migration

If you’re looking at an existing framework with all five of these patterns don’t panic.

You don’t need to rewrite everything at once. A practical approach:

  1. Fix ThreadLocal first — unblocks parallel execution, highest ROI
  2. Add data-testid with your dev team — ongoing, start with the most fragile tests
  3. Remove Thread.sleep one file at a time — replace with explicit waits as you touch each area
  4. Stop swallowing exceptions — a one-pass fix through your wrapper class
  5. Extract components gradually — when a page class gets touched, refactor that section

Incremental improvement beats a big-bang rewrite every time.

Have you run into these patterns in a framework you’ve inherited? I’d love to hear which one caused the most pain — drop a comment below.

--

--

Viranga Bandara
Viranga Bandara

Written by Viranga Bandara

Former Software Quality Engineer @ Cut+Dry | Manual & Automation Testing | Playwright | Selenium | BSc (Hons) IT – University of Moratuwa