Flaky tests are one of the most frustrating problems in browser automation. A test may pass during one run, fail during the next, and then pass again without any code changes. To prevent flaky Selenium tests, teams need to identify the causes of unstable behavior and apply consistent automation practices.
Flakiness reduces trust in test results. Developers may begin ignoring failed tests because they assume the issue is not real. As a result, genuine defects can remain unnoticed.
The good news is that most flaky Selenium tests are caused by a small number of recurring issues. These include poor synchronization, unstable locators, shared test data, inconsistent environments, and tightly coupled test design.
This guide explains how to improve Selenium test reliability step by step.
What Is a Flaky Selenium Test?
A flaky Selenium test produces inconsistent results even though the application and test code have not changed.
For example, the same test may:
- Pass on a developer’s computer
- Fail in a continuous integration pipeline
- Pass when run alone
- Fail when run with the full test suite
- Fail only in one browser
- Pass after being rerun
This inconsistent behavior usually means that the test depends on timing, hidden state, test order, network speed, or browser-specific behavior.
A reliable automated test should produce the same result under the same conditions.
Why Flaky Tests Are a Serious Problem
Flaky tests are more than a technical inconvenience. They can affect the entire development process.
They Reduce Confidence
When tests fail randomly, developers and testers stop trusting automation.
A failed test should signal a real problem. If the same failure is frequently caused by timing or environment issues, the test suite becomes less useful.
They Slow Down Delivery
Teams often rerun failed tests to check whether the problem is real.
This wastes time and delays releases. Engineers may spend hours investigating failures that disappear during the next run.
They Hide Real Defects
A genuine application bug may be dismissed as another flaky test.
This is especially dangerous in large test suites where unstable failures occur regularly.
They Increase Maintenance Costs
Flaky tests require repeated investigation, code changes, and reruns.
Over time, maintaining unreliable automation may cost more than writing the original tests.
Common Causes of Flaky Selenium Tests
Understanding the causes of test instability is the first step toward prevention.
Poor Synchronization
Modern web applications load content asynchronously. Buttons, tables, forms, and messages may appear after JavaScript requests complete.
A Selenium command can run before the element is ready.
For example:
driver.findElement(By.id("submit-button")).click();
This may fail when the button has not yet appeared or is not clickable.
Using fixed delays does not fully solve the problem.
Thread.sleep(5000);
The page may load in two seconds during one run and seven seconds during another. A fixed five-second delay can therefore be either unnecessarily slow or insufficient.
Unstable Locators
Locators that depend on dynamic IDs, long XPath expressions, or layout position are more likely to break.
For example:
By.xpath("/html/body/div[2]/div[3]/form/div[4]/button")
This locator can fail after a small page-layout change.
Dynamic IDs can cause similar problems:
By.id("button_982734")
When the value changes between sessions, Selenium can no longer find the element.
Shared Test Data
Tests may interfere with each other when they use the same account, record, order, or database entry.
For example:
- One test deletes a user that another test expects to find.
- Two tests update the same customer record.
- Several tests use the same login session.
- One test changes settings that affect later tests.
This problem becomes more visible when tests run in parallel.
Test Order Dependency
A test should not require another test to run first.
Consider this sequence:
- Test A creates a customer.
- Test B edits the customer.
- Test C deletes the customer.
Test B will fail when it runs alone because the required customer does not exist.
Each test should prepare its own conditions.
Browser and Environment Differences
Tests may behave differently across browsers because of:
- Rendering differences
- Browser-specific timing
- Cookie behavior
- Window sizing
- Driver versions
- JavaScript execution
- Download handling
The execution environment can also cause instability. Network latency, CPU load, memory usage, and server response time may affect test timing.
Use Explicit Waits Instead of Fixed Delays
One of the most effective ways to prevent flaky Selenium tests is to use explicit waits.
An explicit wait pauses the test until a specific condition is met.
Example in Java:
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement submitButton = wait.until(
ExpectedConditions.elementToBeClickable(By.id("submit-button"))
);
submitButton.click();
This approach waits only as long as necessary.
If the button becomes clickable after two seconds, the test continues immediately. If it is not ready, Selenium keeps checking until the timeout expires.
Useful Expected Conditions
Common Selenium wait conditions include:
- Element is visible
- Element is clickable
- Element is present
- Text is visible
- URL contains a specific value
- Alert is present
- Frame is available
- Element becomes invisible
Example:
wait.until(
ExpectedConditions.visibilityOfElementLocated(
By.cssSelector(".success-message")
)
);
This is more reliable than sleeping for an estimated number of seconds.
The Selenium course catalog includes lessons on Selenium waiting, element interaction, validation, and avoiding flaky Selenium tests.
Create Stable and Readable Locators
Stable locators improve both reliability and maintainability.
Use attributes that are unlikely to change, such as:
- Unique IDs
- Semantic names
- Stable CSS classes
- Custom testing attributes
- Accessible labels
For example:
<button data-testid="save-profile">Save</button>
A reliable locator would be:
By.cssSelector("[data-testid='save-profile']")
Avoid relying on visual position whenever possible.
Prefer Simple Locators
Good:
By.id("email")
Good:
By.name("username")
Good:
By.cssSelector("[data-testid='checkout-button']")
Fragile:
By.xpath("//div[3]/div[2]/span/button")
A stable locator should describe the purpose of the element, not its temporary position on the page.
Wait for Application State, Not Only Elements
An element may exist in the DOM but still not be ready for interaction.
For example, a button may be visible while an invisible loading layer blocks clicks.
Instead of checking only for presence, wait for the real application state.
You may need to wait for:
- A loading spinner to disappear
- A button to become enabled
- A table row to appear
- An API-driven message to display
- A page transition to finish
- A modal animation to complete
Example:
wait.until(
ExpectedConditions.invisibilityOfElementLocated(
By.cssSelector(".loading-spinner")
)
);
Then continue with the next action.
This makes the test match actual user behavior more closely.
Keep Tests Independent
Each test should be able to run by itself.
A reliable test should:
- Prepare its own data.
- Open the required page.
- Complete its own actions.
- Verify the result.
- Clean up created data where necessary.
Avoid depending on another test to create the required state.
For example, instead of relying on a previous test to create a user, create a unique user at the beginning of the current test.
String uniqueEmail = "test_" + System.currentTimeMillis() + "@example.com";
This reduces collisions and makes parallel execution safer.
Use Clean and Predictable Test Data
Good test data should be controlled, unique, and easy to reset.
Do not use one shared account for every scenario.
Instead, create separate data for:
- Standard users
- Administrators
- Read-only users
- Invalid login scenarios
- Empty-state scenarios
- Large-data scenarios
Use APIs or database setup tools when available. Preparing test data through the user interface can make every test slower and more fragile.
For example, use an API to create a customer record before testing the customer-editing page.
This allows the browser test to focus on the interface behavior that actually needs validation.
Avoid Overly Broad Assertions
Assertions should verify one clear outcome.
An unstable test may check too many unrelated details at once.
For example:
assertTrue(pageSource.contains("Success"));
This can be unreliable because the word “Success” may appear somewhere unrelated on the page.
A more precise assertion would be:
WebElement message = wait.until(
ExpectedConditions.visibilityOfElementLocated(
By.cssSelector(".alert-success")
)
);
assertEquals("Profile updated successfully.", message.getText());
Precise checks make failures easier to understand.
They also reduce false positives.
Use the Page Object Model
The Page Object Model separates page-specific details from test logic.
Instead of placing locators and browser actions directly inside every test, create page classes.
Example:
public class LoginPage {
private WebDriver driver;
private WebDriverWait wait;
private By usernameInput = By.id("username");
private By passwordInput = By.id("password");
private By loginButton = By.id("login-button");
public LoginPage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
public void login(String username, String password) {
wait.until(
ExpectedConditions.visibilityOfElementLocated(usernameInput)
).sendKeys(username);
driver.findElement(passwordInput).sendKeys(password);
wait.until(
ExpectedConditions.elementToBeClickable(loginButton)
).click();
}
}
The test becomes cleaner:
LoginPage loginPage = new LoginPage(driver);
loginPage.login("testuser", "password");
This structure reduces duplicated code.
It also gives teams one place to improve waits and locators when the application changes.
Selenium Academy covers design patterns as part of its Selenium curriculum, helping learners build more maintainable automation frameworks.
Control Browser State
Unexpected browser state can cause flaky behavior.
Before each test, consider resetting:
- Cookies
- Local storage
- Session storage
- Browser cache
- Open tabs
- Window size
- User sessions
A test should not depend on data left behind by a previous test.
Example:
driver.manage().deleteAllCookies();
You can also start a fresh browser session for each test when execution time allows.
This improves isolation and reduces hidden dependencies.
Use Consistent Browser and Driver Versions
Mismatched browser and driver versions can create unstable failures.
Keep the following components compatible:
- Browser version
- WebDriver version
- Selenium version
- Operating system environment
- Test framework version
Document the versions used in local development and continuous integration.
When possible, manage dependencies automatically rather than downloading drivers manually.
Also review failures by browser. A test that fails only in one browser may indicate a real compatibility issue rather than test flakiness.
Configure Timeouts Carefully
Timeouts that are too short can create false failures.
Timeouts that are too long can make broken tests slow.
Define timeouts based on realistic application behavior.
For example:
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
Use longer waits only for operations that genuinely require more time, such as report generation or file processing.
Avoid using one very long global timeout for every action.
Handle Animations and Dynamic Interfaces
Animations can cause Selenium to interact with elements while they are moving.
This may lead to errors such as:
- Element click intercepted
- Element not interactable
- Stale element reference
- Move target out of bounds
Wait for animations to complete before interacting.
Where appropriate, development teams can disable nonessential animations in test environments.
You can also wait until an element reaches a stable state.
Do not immediately retry every failed click without understanding the cause. Blind retries can hide genuine interface problems.
Handle Stale Element References Correctly
A stale element error occurs when Selenium stores a reference to an element that is later replaced in the DOM.
For example:
WebElement saveButton = driver.findElement(By.id("save"));
If the page refreshes part of the interface, that stored element may no longer be valid.
Locate the element again after the update:
wait.until(
ExpectedConditions.elementToBeClickable(By.id("save"))
).click();
Avoid keeping element references for longer than necessary on highly dynamic pages.
Use Retries Carefully
Retries can be useful for diagnosing intermittent infrastructure issues. However, they should not become the main solution for flaky tests.
A test that passes only after three attempts is still unstable.
Use retries to:
- Collect more failure evidence
- Identify patterns
- Reduce temporary pipeline disruption
- Investigate infrastructure problems
Do not use retries to hide poor synchronization, weak locators, or shared data.
Track how often tests require retries. Frequent retries are a signal that the underlying issue still needs correction.
Capture Useful Failure Evidence
When a test fails, collect enough information to understand why.
Helpful evidence includes:
- Screenshot
- Current URL
- Browser name and version
- Error message
- Page source
- Console logs
- Network errors
- Test data identifiers
- Execution timestamp
Example screenshot code:
File screenshot = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.FILE);
Failure evidence helps teams distinguish between:
- A real application defect
- A locator failure
- A synchronization problem
- An environment issue
- A test data conflict
Selenium Academy includes lessons on taking screenshots with Selenium and handling common browser interactions.
Run Tests in Different Execution Modes
Some flaky tests fail only under specific conditions.
Run tests:
- Individually
- As part of the full suite
- In random order
- In parallel
- Across different browsers
- In the continuous integration environment
If a test passes alone but fails in the full suite, it may depend on shared state or execution order.
If it fails only in parallel, it may have test data collisions.
If it fails only in one browser, investigate browser-specific behavior.
Review Flaky Tests as a Team
Do not allow unstable tests to remain in the suite indefinitely.
Create a process for:
- Identifying flaky tests
- Recording failure frequency
- Assigning ownership
- Investigating the root cause
- Fixing or temporarily isolating the test
- Restoring it after validation
A test should not be permanently ignored just because it fails unpredictably.
Flakiness is a quality problem and should be tracked like other defects.
A Practical Flaky Test Prevention Checklist
Before writing the test:
- Choose stable locators.
- Define the required test data.
- Confirm that the test can run independently.
- Identify asynchronous page behavior.
- Plan meaningful assertions.
During implementation:
- Use explicit waits.
- Avoid fixed delays.
- Keep page actions in Page Objects.
- Reset browser state.
- Use unique test data.
- Wait for loading indicators to disappear.
During execution:
- Record browser and driver versions.
- Capture screenshots on failure.
- Monitor retry frequency.
- Test in different browsers.
- Run the suite in random order.
- Check for parallel execution conflicts.
During maintenance:
- Remove outdated tests.
- Update unstable locators.
- Review recurring failures.
- Refactor duplicated waits.
- Improve test data management.
- Investigate failures instead of simply rerunning tests.
Learn Reliable Selenium Automation With Selenium Academy
Preventing flaky tests requires a strong understanding of Selenium WebDriver, waits, element interaction, validation, design patterns, browser behavior, and automation architecture.
The Selenium Academy Selenium course covers topics such as:
- Selenium WebDriver
- Identifying elements
- Element interaction
- Selenium waits
- Validation
- Design patterns
- Avoiding flaky Selenium tests
- Screenshots
- Cross-browser testing
- Selenium Grid
- Jenkins and TeamCity integration
Training is available with detailed text explanations and English and Turkish subtitle options.
Selenium courses are taught using Java, C#, Ruby, Python, and JavaScript. Learners can review the available options on the Selenium Academy pricing page.
Conclusion
Flaky tests are usually caused by poor synchronization, unstable locators, shared data, hidden dependencies, or inconsistent environments.
To prevent flaky Selenium tests, use explicit waits, create stable locators, isolate test data, reset browser state, and design independent test cases. In addition, use Page Objects, collect useful failure evidence, and investigate repeated failures instead of relying on retries.
Reliable automation does not come from adding more delays. It comes from building tests that understand the real state of the application.
By learning Selenium waits, element interaction, design patterns, validation, and cross-browser testing, QA professionals can create test suites that produce consistent and trustworthy results.
Frequently Asked Questions
What causes flaky Selenium tests?
Common causes include poor synchronization, unstable locators, dynamic page content, shared test data, browser differences, and test order dependency.
Should I use Thread.sleep in Selenium tests?
Fixed delays should generally be avoided.
Use explicit waits that continue when a specific condition is met. This makes tests faster and more reliable.
What is the best way to wait for elements in Selenium?
Use WebDriverWait with ExpectedConditions, such as visibility, clickability, presence, or invisibility.
Choose the condition that reflects the actual state required by the test.
Can retries fix flaky Selenium tests?
Retries may reduce temporary pipeline disruption, but they do not fix the underlying problem.
Frequent retries can hide synchronization, locator, or environment issues.
How does the Page Object Model reduce flaky tests?
The Page Object Model centralizes locators, waits, and page actions.
This reduces duplicated code and makes it easier to update unstable interactions consistently.
How can I detect test order dependency?
Run tests individually and in random order.
A test that fails only when executed alone or after another test probably depends on shared state.
