An automated test failure is a reason to investigate, not proof by itself that the application regressed. The cause may be a product defect, a flaky test, uncontrolled test data, an external dependency, or an unstable runner. Preserve evidence first, then use reproducibility, logs, and the failure conditions to identify which layer needs fixing.
This guide covers why automated tests fail, why tests may pass locally but fail in CI, and how to fix flaky UI and other automated tests without hiding real defects.
As an Amazon Associate I earn from qualifying purchases.
Why are my automated tests failing?
Start by locating the failure in the system, not by assuming the assertion is wrong. A useful breakdown separates test setup and data, test execution and scheduling, the application and its dependencies, and the operating system, hardware, or network. Google’s 2021 discussion of test flakiness describes causes across these layers (Google Testing Blog).
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →- Test or data problem: setup is incomplete, cleanup leaves state behind, tests depend on order, or parallel workers collide on shared records.
- Timing or scheduling problem: an action runs before the application is ready, asynchronous events arrive in a different order, or concurrent work exposes a race.
- Application or dependency problem: the system is slow, unresponsive, resource-starved, changed unexpectedly, or depends on a service that is failing.
- Execution environment problem: the runner lacks capacity, another process competes for resources, or network, OS, browser, or hardware conditions differ.
A failure that reproduces on the same application version in a controlled environment is stronger evidence of a product defect than one that appears only under a particular order, concurrency level, or runner. Neither outcome alone proves the cause; compare the failed execution with a successful one.
How to triage a failed run
- Preserve the failure before rerunning. Save the test and application versions, timestamps, relevant logs, screenshot or trace, environment details, and identifiers for test data. A rerun can overwrite the evidence that explains the original failure. Google’s end-to-end testing guidance recommends retaining relevant state, and Chromium recommends targeted logging and comparing successful and failed runs (Google Testing Blog; Chromium).
- Run the failing test alone. If it passes, repeat it in its original group/order and at the original concurrency. That contrast can expose order dependence, shared state, cleanup gaps, and data collisions. pytest notes that failures in parallel can indicate order dependence (pytest: Flaky tests).
- Check whether the application reached the required state. Inspect request and response timing, application logs, and the state at the failed action. Ask whether the test waited for the right condition rather than merely waiting for some time.
- Inspect what the test actually saw. Review the DOM, screenshot, or trace at failure time. Determine whether the element was missing, obscured, disabled, renamed, or selected through a changed implementation detail.
- Compare environments if the failure is CI-only. Check runner capacity, CPU and memory pressure, concurrency, OS and browser versions, network errors, and competing processes. Reproduce under comparable resource and concurrency conditions where possible.
- Classify and repair the cause. Record whether the evidence points to a product defect, test-code defect, state coupling, external dependency, or infrastructure problem. Keep a regression test for the repaired behavior.
Retries can help reveal whether a failure is intermittent, but a passing retry does not identify its cause. pytest describes reruns as a mitigation and cautions that permanent quarantine can be dangerous. Track retry outcomes rather than allowing a green retry to erase the original failure.
How do I fix flaky tests?
A flaky test produces different outcomes without a relevant intended change. Often the test depends on state or timing it does not control. Use the pattern of failures—alone versus in a suite, serial versus parallel, local versus CI—to narrow the cause before changing the test.
Timing and synchronization errors
A test may click before a control is ready, wait for an unrelated signal, or assume asynchronous events always arrive in the same order. Selenium identifies races between the browser and WebDriver as a source of automation problems (Selenium: Overview of Test Automation).
- Triage: add timestamps around actions and requests, retain the failing trace, and inspect which condition had or had not occurred when the test proceeded. A controlled delay may help determine whether timing is involved, but it is an experiment, not a repair.
- Fix: wait for an observable, condition-specific state and assert that state with a real timeout. Use framework actionability checks where available. Avoid arbitrary fixed sleeps: Google warns that they can become flaky over time and slow the suite, while Playwright recommends waiting on the actual condition (Google Testing Blog; Playwright Best Practices).
Shared or dirty state, test data, and cleanup
A test can depend on records created by another test, leave global state modified, or collide with another worker using the same data. Such coupling often becomes visible only when tests run in a different order or in parallel. pytest and Playwright both emphasize isolation; Playwright’s guidance says tests should run independently with their own storage and data (pytest; Playwright).
- Triage: run the case alone, in its original order, and in parallel; compare a clean environment with a reused one; inspect setup, teardown, database records, and shared browser storage.
- Fix: create prerequisites explicitly, use unique or isolated data, restore modified global state, and make cleanup reliable. If isolation is not yet possible, prevent the affected tests from running concurrently while you remove the coupling. Chromium documents scoped state setters and explicit resets as ways to prevent global-state flakes (Chromium).
Brittle locators and implementation-coupled assertions
Selectors based on DOM position, CSS classes, or incidental markup can break after a harmless redesign. The test may also assert an internal detail instead of what a user can observe. Playwright recommends user-facing attributes or an explicit stable contract, and testing rendered behavior rather than implementation internals (Playwright Best Practices).
- Triage: inspect the page or trace at failure time. Check whether the control is absent, covered, disabled, relabeled, or simply reached through an obsolete selector. Verify that the assertion reflects the behavior the test is meant to protect.
- Fix: choose accessible roles, labels, or other user-facing attributes when they represent the intended contract. Where visible text or structure changes independently of the behavior under test, use an explicit stable test contract. No selector strategy is universally stable; match it to the behavior being tested.
Third-party services and uncontrolled dependencies
A test that relies on an external service inherits its latency, outages, content changes, and consent banners. That can obscure whether the feature your team owns is working. Playwright recommends focusing tests on what the team controls, while Google’s end-to-end guidance notes that external components can change unexpectedly and test doubles can drift from real contracts (Playwright; Google Testing Blog).
- Triage: identify requests to services outside the feature under test and compare their response and timing in failed and controlled runs.
- Fix: stub or intercept dependencies when testing owned behavior, and keep separate integration coverage for cases where the real dependency matters. Keep fakes aligned with the real interface so they do not conceal incompatibilities.
Runner, resource, or machine instability
A CI runner may not have enough capacity to start or sustain the application; other processes may compete for resources; tests may collide when scheduled together; or network and machine faults may interrupt execution. Google’s failure taxonomy includes these environment and execution sources (Google Testing Blog).
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11- Triage: check that the application started, inspect runner and system logs, compare resource behavior, and review network or hardware errors. Reproduce at similar concurrency when that matches the observed failure. Chromium recommends parallel stress when it is useful for exposing races or resource contention (Chromium).
- Fix: provide adequate runner capacity, reduce unrelated load, correct scheduling collisions, or isolate resources. A concurrency-only failure can indicate shared data or state coupling as well as insufficient capacity.
Real application or dependency defects
Not every intermittent failure is test noise. The application or a dependency may be racy, slow, unresponsive, resource-starved, or changed without an appropriate test update. Compare a failure and success on the same version, inspect application and dependency logs, and try to reproduce under controlled conditions. Chromium recommends comparing paths and debugging reproducible failures (Chromium).
If the evidence points to a behavior regression, repair the system and retain a regression test. Do not weaken an assertion simply to restore a green build when the test is correctly exposing a product defect. If intended behavior changed, update the test to the new contract.
Why do my tests pass locally but fail in CI?
CI changes more than the machine name. It can change available CPU and memory, concurrency, scheduling, browser or OS versions, network conditions, environment variables, and the freshness of data. Treat the local/CI difference as a clue about which condition is uncontrolled, not as proof that CI itself is faulty.
- Confirm the same application revision, test inputs, browser, and relevant environment configuration are being used.
- Check whether the application starts successfully and whether startup or readiness differs under runner load.
- Compare CPU, memory, process contention, parallel-worker count, network logs, and machine errors.
- Try the failing case alone and at CI-like concurrency. If only parallel execution triggers it, investigate shared state and data collisions as well as runner capacity.
- For visual comparisons, keep browser and OS versions consistent; otherwise rendering differences may be environmental rather than application changes.
Change one condition at a time where practical and retain the original failure artifacts. This makes it easier to distinguish an environment mismatch from a race in the application or test.
When to use end-to-end tests—and when not to
Browser and end-to-end tests are valuable when a critical user workflow crosses components and cannot be reliably verified at a lower level. They also carry higher execution and maintenance costs. Google’s guidance says end-to-end tests are slower, more flaky, and more expensive to maintain than unit or integration tests; Selenium similarly recommends using the browser only when needed (Google Testing Blog; Selenium).
| Choose coverage based on | Questions to ask |
|---|---|
| Behavior scope | Does this require a real browser and interaction across components, or can a unit or integration check prove it? |
| Control | Can the test own its data and dependencies, or does it inherit external state and service behavior? |
| Isolation | Can it run repeatedly and independently without relying on ordering or shared resources? |
| Cost and speed | Is the user risk high enough to justify the slower run and ongoing maintenance? |
| Diagnostics | Will logs, traces, screenshots, or retained state make a failure actionable? |
Keep browser cases focused on important workflows, preserve relevant artifacts, and use ephemeral test data to limit side effects. A smaller, well-diagnosed end-to-end suite is usually more useful than broad coverage whose failures are difficult to explain.
Rank #4
Capture a browser screenshot as failure evidence
A screenshot can help establish what the browser rendered at a particular point in a failed run. It is one artifact, not a substitute for a trace, logs, request timings, or application state: a static image cannot explain what happened before capture or identify the root cause by itself. Capture at the failure point and retain it with the run identifier and environment details.
Do it yourself with a browser automation framework
In Playwright’s JavaScript test runner, capture the current page in the failure handler or at the relevant checkpoint:
Free tools Windows power users keep installed
One-click scans. No signup required.
import { test, expect } from '@playwright/test';
test('checkout shows confirmation', async ({ page }) => {
await page.goto('https://example.com/checkout');
try {
await expect(page.getByRole('heading', { name: 'Order confirmed' }))
.toBeVisible({ timeout: 10_000 });
} catch (error) {
await page.screenshot({ path: 'checkout-failure.png', fullPage: true });
throw error;
}
});
Replace the example URL and expected heading with the application and user-visible outcome under test. Configure the test environment to retain the generated file as a CI artifact. If your framework already attaches screenshots or traces on failure, use that facility rather than collecting duplicate files.
Or skip the browser setup
For an independent screenshot of a URL, ScreenshotNeo provides a one-call screenshot API. This can add a page image to diagnostics, but it does not replace the failed test’s own trace or prove why the test failed.
Best Value
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
See the ScreenshotNeo API documentation for request options. Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. Learn more at ScreenshotNeo, or sign up for the free plan.
Troubleshooting: common symptoms and next checks
| Symptom | Likely area to investigate | Next check |
|---|---|---|
| Passes alone, fails in suite | Order dependence, shared state, incomplete cleanup | Restore suite order; inspect shared records and setup/teardown. |
| Fails only in parallel | Data collisions, global state, resource contention | Use unique data, inspect shared resources, compare concurrency and runner load. |
| Fails after a fixed wait | Wrong or insufficient synchronization condition | Wait for the application state the next action requires and inspect request timing. |
| Element lookup fails after a UI change | Brittle selector or changed user-facing contract | Inspect rendered output and select by the intended accessible or explicit stable contract. |
| Fails only in CI | Runner, environment mismatch, scheduling, network | Compare versions, resources, startup, concurrency, and machine/network logs. |
| Failure follows an external request | Service latency, outage, changed content, or uncontrolled response | Inspect that request; stub owned-behavior tests and retain integration coverage where needed. |
| Failure reproduces on a controlled run | Potential application or dependency defect | Compare success and failure paths and inspect system logs before changing the assertion. |
Prevent repeat failures without hiding them
- Make each test’s prerequisites and data explicit, and isolate storage and mutable state.
- Use condition-based waits and assertions tied to observable behavior rather than arbitrary delay.
- Keep third-party dependencies controlled in tests of owned behavior, with separate coverage for real integrations where they matter.
- Keep browser coverage focused on critical cross-component behavior and retain failure artifacts.
- Track retries and quarantines visibly. A retry may reduce disruption temporarily, but it is not a root-cause fix.
- When behavior intentionally changes, update the test contract; when it regresses, fix the system rather than weakening a valid check.
Frequently Asked Questions
Why does a test pass on retry after failing once?
The first run may have encountered a transient timing, state, dependency, or resource condition. A successful retry shows the result is intermittent; it does not identify which condition caused it.
Should I quarantine a flaky test?
Only as a visible, temporary containment measure when needed to unblock work. Keep ownership and failure reporting, because permanently hiding the test can conceal regressions.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




