# How to Stabilize Automated Tests

Stabilize flaky automated tests with better waits, reliable locators, isolated data, useful logs, and dependable cross-browser infrastructure.

Published July 2, 2026 by Liviu Lupei on the Endtest blog. Canonical URL: https://endtest.io/blog/how-to-stabilize-automated-tests

---
Flaky automated tests are expensive in a way that rarely appears in a budget.

A test fails, so someone runs it again. It passes the second time, so the failure is ignored. After this happens often enough, a red test result no longer means that the release should stop. It means that someone should click **Retry**.

At that point, you do not really have an automated test suite. You have a notification system that may or may not be telling the truth.

Teams often blame the framework. Selenium is flaky. Playwright is flaky. Cypress is flaky. The testing platform is flaky.

Framework and browser bugs do happen. But in my experience, most flaky tests are caused by something less dramatic: waits that are not synchronized with the application, locators that are too fragile, test data that depends on a previous run, an overloaded environment, or settings that were never configured for the real behavior of the application.

Changing tools without fixing those problems usually moves the same instability into a new syntax.

## Define what a stable test actually is

A test that passes is not necessarily a stable test.

If the product is broken, a good test should fail. Stability means that the test reaches the same conclusion when the relevant conditions are the same.

My practical definition is simple: if you run a test 5,000 times under the same conditions, the result should be the same 5,000 times.

When the feature works, it should pass consistently. When the feature is broken, it should fail consistently and at the correct step.

Even a 99.9% consistency rate produces roughly five questionable results across 5,000 executions. That might sound acceptable until those failures appear in the middle of release pipelines, wake people up during monitoring runs, or cause the team to stop trusting the entire suite.

The goal is to make every result believable rather than to make the dashboard green.

## Optimize waits instead of adding delays

Poor synchronization is probably the most common cause of flaky browser tests.

Modern applications rarely load in one clean event. The page may appear before a request finishes. A button may be visible before it is enabled. A dropdown may exist in the DOM while an animation still prevents a click. A React component may render, disappear, and render again.

The tempting fix is to add a five-second pause.

That can make the test pass, but it does not make the test stable. It only assumes that the application will always finish within five seconds. It also makes every fast execution wait longer than necessary.

A better test waits for the condition it actually needs:

- The element is visible and enabled.
- The loading indicator has disappeared.
- The expected URL or text is present.
- A network request has completed.
- The number of results has changed.
- The animation or background job has finished.

Hardcoded pauses still have a place when you intentionally need to test a duration, such as a session timeout. They should not be the default solution for ordinary synchronization.

This is also why increasing every timeout globally is usually the wrong first move. It can hide the real issue while making the entire suite slower. As discussed in [our guide to speeding up test executions](https://endtest.io/blog/5-ways-to-speed-up-test-executions), the best waits continuously check for the required condition and continue as soon as it is met.

## Use locators that describe intent

A locator can be technically correct and still be unreliable.

Long XPath expressions, `nth-child` selectors, generated CSS classes, and exact matches against frequently edited text can all work today and fail after a harmless UI change tomorrow.

Whenever possible, prefer locators based on stable application intent:

- Unique IDs created for testing
- `data-testid` or similar dedicated attributes
- Stable names and labels
- Accessible roles combined with reliable accessible names
- Small, deliberate relationships between nearby elements

The best locator is the one that the development and testing teams can look at six months later and immediately understand, which is rarely the cleverest one.

Self-healing can reduce maintenance when an element changes, but it should not be used to excuse ambiguous selectors. If three buttons look almost identical to the automation, the underlying test is still unclear. AI cannot reliably infer intent that was never expressed.

Reliable locators should also be unique. A selector that sometimes matches one element and sometimes matches three is not stable, even if the framework happens to click the correct one during your local run.

## Make every test own its state

Many flaky tests are not actually flaky in isolation. They are dependent on invisible state left behind by another test.

A sign-up test assumes the email address has never been used. A checkout test assumes the cart is empty. A project-creation test assumes no project already has that name. The first run passes, and the second run fails.

The most reliable testing flows create the users, accounts, projects, orders, and other data they need. They use unique values, verify that setup succeeded, and clean up afterward when cleanup is practical.

This makes the flow independent from previous executions and from the order in which tests run.

Sometimes reusing existing data is unavoidable. In that case, the flow should be designed for the states it may encounter. It might need to detect that a user is already verified, empty an existing cart, remove an old project, or restore the application to a known starting point.

The key is determinism. The test should not be surprised by a state that the team already knows can exist.

This principle matters even more when tests run in parallel. Two tests should not modify the same user, order, inbox, or shared record unless the collision is part of the scenario being tested.

## A stable test needs a stable environment

You cannot build stable automation on top of an environment that changes behavior from minute to minute.

A staging server with insufficient CPU may respond quickly during the day and time out when the full suite runs. Shared databases can accumulate bad data. Background workers can fall behind. Email and SMS providers can throttle requests. Rate limits can turn parallel execution into random failures.

Before changing the test, check whether the environment is giving the test a consistent application to evaluate.

The same applies to browser infrastructure. Browser and driver versions should be compatible. Operating systems, fonts, permissions, viewport sizes, locale, and time zone should be controlled. Parallel workers need enough CPU and memory. Test sessions should be isolated from one another.

Running on your laptop can hide these problems because your local environment is familiar and lightly loaded. CI and cross-browser execution expose the assumptions.

This is one reason we invested heavily in Endtest's [cross-browser testing infrastructure](https://endtest.io/product/validate/cross-browser-testing). A managed platform does not eliminate the need for good test design, but dependable browser machines, real operating systems, and consistent session configuration remove a major source of unnecessary variance.

## Collect enough evidence to explain every failure

A screenshot is useful, but it is rarely enough.

To diagnose stability problems, collect the evidence that explains what happened before and during the failure:

- The failing step and its duration
- Screenshots and video
- Browser console messages
- Network requests and response codes
- Browser, driver, operating system, and viewport details
- Test data identifiers
- Retry history
- Relevant application and server logs

The most important evidence is often the difference between a passing run and a failing run.

Did the failing run receive a slower API response? Did the locator match a different element? Did the browser console report a JavaScript exception? Did a previous test leave the user in a different state?

A test that passes on retry should still be investigated. The retry is proof that the original result was not deterministic. Retries are a useful safety net for infrastructure incidents, but they are not a stabilization strategy.

Endtest's [test insights and analytics](https://endtest.io/product/analyze/test-insights) are designed around this idea: a failure should include enough context to classify it instead of forcing someone to reproduce it blindly.

## Use a repeatable stabilization loop

When a test is unreliable, avoid making several speculative changes at once. Use a small loop:

1. Reproduce the failure with the same browser, data, environment, and configuration.
2. Classify it as synchronization, locator, data, environment, infrastructure, configuration, or a real product defect.
3. Fix the root cause rather than increasing delays or retries.
4. Run the test repeatedly and under parallel load.
5. Track whether the same failure pattern returns.

Temporary quarantine can be reasonable when one test is blocking the entire pipeline, but quarantine needs an owner and an expiration date. Otherwise, the flaky-test folder becomes the place where useful coverage goes to disappear.

The same is true for broad exception handling. A test that catches every error and continues may look stable, but it is only hiding failures.

## Stability is a property of the whole system

Stable automation comes from the combination of clear test logic, condition-based waits, reliable locators, controlled data, dependable environments, consistent browser infrastructure, and useful diagnostics, rather than from one framework feature.

At Endtest, we have seen teams replace one framework with another and carry the same flaky tests with them. The syntax changed, but the waits, selectors, shared data, and environment assumptions did not.

AI, self-healing, and managed infrastructure can remove a great deal of repetitive maintenance. They work best when the underlying flow is deterministic and the expected behavior is clear.

A good automated test should be boring. It should run thousands of times, reach the same conclusion, and give the team a signal they can act on without first asking whether the test itself is telling the truth.
