ScreenshotNeo

BlogGuides

Cypress Best Practices for Reliable Tests

Build Cypress tests that run independently, wait for real application state, and fail for useful reasons. Includes selectors, isolation, retries, CI, and debugging.

By the ScreenshotNeo team4 October 20269 min read

Reliable Cypress tests are independent, use selectors that survive styling changes, and wait for observable application state instead of guessed delays. Keep retries intentional: a test that passes only after a retry has exposed instability worth investigating. In CI, wait until the application server is ready before starting Cypress.

This guide shows a practical setup and runnable Cypress examples, then covers isolation, selectors, asynchronous UI, retries, CI, troubleshooting, and trade-offs.

1. Start with independent tests

Each test should establish the state it needs, perform one behavior, and assert the result. A test that depends on a previous test’s login, database row, or browser state may pass in a full suite but fail when run alone or in a different order. Cypress recommends that tests run independently and still pass.

Put shared setup in hooks only when it applies to each test. Keep the actual state requirement visible. For example, create a user through an API or seed mechanism when the behavior under test is the page interaction, rather than relying on another test to create the user.

describe('checkout', () => {
  beforeEach(() => {
    // Establish the state this test needs, for example through a test API.
    cy.request('POST', '/test-support/reset-cart');
    cy.visit('/checkout');
  });

  it('shows a validation message when email is missing', () => {
    cy.get('[data-cy="place-order"]').click();
    cy.get('[data-cy="email-error"]')
      .should('be.visible')
      .and('contain', 'Enter your email');
  });
});

Adapt the test-support route and selectors to your application. Do not expose destructive test-support endpoints in production.

What Cypress resets between tests

With end-to-end testIsolation: true, Cypress visits about:blank and clears cookies, localStorage, and sessionStorage before each test. Cypress also resets aliases, clock mocks, intercepts, spies, stubs, and viewport changes between tests. IndexedDB and other storage mechanisms are not cleared by that browser reset, so applications using them may need explicit cleanup.

Component tests reset the rendered component and the named browser stores. Cypress documents that testIsolation configuration is not supported for component testing.

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    testIsolation: true,
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.js'
  }
});

Keep isolation enabled unless you have a measured reason to change it. If you consider testIsolation: false for a suite, first verify that its tests pass individually and in different orders. Disabling isolation can reduce repeated setup, but it also allows state leakage.

2. Choose selectors that express intent

Use dedicated data-* attributes, such as data-cy, for elements that tests need to find. They are separate from styling and application behavior, so a CSS redesign is less likely to break the test. Avoid selectors based on broad tags, DOM position, or styling classes.

<button data-cy="save-profile">Save profile</button>

// cypress/e2e/profile.cy.js
cy.get('[data-cy="save-profile"]').click();
cy.get('[data-cy="save-confirmation"]').should('be.visible');

Use visible text when the wording itself is part of what you are checking. For example, a test of a changed consent label should locate or assert that text. For a generic interaction, a stable test attribute avoids coupling the test to copy that may change for editorial or localization reasons.

Selector Good fit Risk
[data-cy="submit"] Finding a control to interact with Requires adding and maintaining a test attribute
Visible text Verifying user-facing wording or locating a uniquely meaningful label Copy and localization changes can affect the test
Styling class Rarely appropriate for behavior tests Refactors and design changes can break the selector
Generic tag or positional selector Only when the structure itself is the behavior under test Often matches the wrong element as the page grows

The cypress/require-data-selectors rule from eslint-plugin-cypress can help enforce data attributes. Configure it if your team wants linting to catch selectors that do not follow your convention.

3. Synchronize on UI state, not guessed time

Cypress retries linked queries and assertions while they wait for the expected UI state, up to the applicable timeout. This makes a query such as cy.get(...).should(...) useful for asynchronously rendered content. Fixed sleeps do not observe the application: they can waste time when it is fast and still be too short when it is slow.

// Prefer an assertion that waits for the rendered result.
cy.get('[data-cy="results"]')
  .should('be.visible')
  .and('contain', 'Order confirmed');

// Avoid using a guessed delay as synchronization.
// cy.wait(5000);

Queries and assertions can be retried; actions such as .click() run once. End an action chain after the action, then begin a fresh query to check its effect. This avoids relying on a subject that the action may have caused the application to replace.

// Action, then a fresh query and assertion.
cy.get('[data-cy="open-settings"]').click();
cy.get('[data-cy="settings-panel"]').should('be.visible');

Do not treat retry-ability as permission to repeat arbitrary commands. If an action is not safe to repeat, keep it outside retrying logic and assert its outcome through a new query.

Wait for a specific network request when it is the right boundary

When a test depends on a request completing, intercept that request and wait for its alias rather than sleeping for a duration. Then assert on the rendered state too: the request completing does not by itself prove the page displayed the intended result.

cy.intercept('GET', '/api/orders').as('loadOrders');
cy.visit('/orders');
cy.wait('@loadOrders');
cy.get('[data-cy="orders-list"]').should('be.visible');

Use this when the request is a meaningful dependency of the behavior. If the UI can update without a network request, wait on the UI condition instead. Avoid tests that depend on incidental requests that are not part of the behavior being verified.

4. Use retries to find instability

Cypress test retries are off by default. You can enable a small, intentional number to help identify intermittent failures or reduce disruption from transient problems, but a pass on retry is still evidence that the test was unstable on its first attempt. Track retrying tests and investigate race conditions, state leakage, or unstable dependencies.

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  retries: {
    runMode: 2,
    openMode: 0
  },
  e2e: {
    testIsolation: true
  }
});

These values are an example policy, not a universal recommendation. Choose retries based on how you run locally and in CI. Retries add execution time, and a high retry count can make an unreliable suite look healthier than it is if retry passes are ignored.

5. Make CI wait for the app to be ready

Start the test server and verify that it responds before invoking Cypress. Running Cypress immediately after a background server command creates a startup race. A fixed sleep is also unreliable because startup time varies. Cypress’s GitHub Action supports start and wait-on options for workflows that use that action.

# Example shell workflow when the project provides these scripts:
npm run start:test &
npx wait-on http://localhost:3000
npx cypress run

The example uses wait-on; install and configure it in the project if it is not already available. With Cypress’s GitHub Action, use its documented server startup and readiness options instead when they fit your workflow.

Run the suite on pushes or pull requests so failures are visible near the change that introduced them. When a CI-only failure persists, compare the browser and environment with local runs, inspect available screenshots or video, use Test Replay where available, and reduce the failure to a smaller reproducer.

6. Common failures and fixes

Symptom Likely cause What to do
Passes in the suite but fails alone It relies on earlier state, order, or data Establish its own prerequisites; run it alone and in a different order; inspect shared server-side state.
Element not found intermittently The test queries before rendering completes, or the selector is fragile Use a stable data attribute and a retryable query/assertion for the expected state.
Click succeeds but expected change is missing The test assumes the action chain retries or checks stale state End the action chain, start a fresh query, and assert the visible result.
Arbitrary wait fixes a failure temporarily The test is synchronized to elapsed time instead of app state Wait for a specific UI assertion or, when relevant, an aliased request; remove the guessed delay.
CI cannot connect to the app Cypress started before the server was ready, or the configured URL is wrong Check the base URL and server logs; gate Cypress on a readiness check.
Failure occurs only after a retry Race condition, leaked state, or intermittent dependency Record the retry, inspect failure artifacts, and investigate rather than treating the retry pass as a clean result.
State survives browser isolation The app stores it in IndexedDB or another mechanism Cypress does not clear in this reset Clear or seed that store explicitly as part of test setup.

7. Performance, reliability, and cost trade-offs

  • Test setup: Programmatic setup can avoid repeating slow UI flows when those flows are not the subject of the test. Keep the preconditions explicit so a test remains understandable.
  • Isolation: Independent tests are easier to rerun and diagnose. Disabling isolation may save setup time but increases the chance that one test changes another test’s outcome.
  • Waiting: State-based assertions avoid arbitrary delay overhead and are more resilient to timing variation. Network waits are useful only when that request is a real dependency.
  • Retries: Retries can reduce immediate CI disruption from transient failures, but each retry costs time and can hide instability if teams do not review retry results.
  • Failure artifacts: Screenshots, video, and replay tools can make failures easier to investigate, but they add workflow and artifact handling overhead. Use them where they help diagnose the suite.

8. A practical reliability checklist

  • Each test sets up the state it needs and can pass on its own.
  • Selectors use stable data-* attributes unless visible text is the behavior being checked.
  • Assertions wait for observable state instead of a guessed duration.
  • Actions end their query chain; a fresh query verifies the outcome.
  • Retries are limited, visible, and investigated when they are used.
  • CI confirms server readiness before starting Cypress.
  • Failures can be reduced to a small reproducer, with artifacts reviewed when available.

Or skip the browser setup

If a test or CI job also needs a screenshot of a page, ScreenshotNeo is a website screenshot API and MCP server. It can return a PNG, JPEG, WebP, or PDF from one GET request. See the ScreenshotNeo API documentation for options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Cookie banners are accepted like a visitor would accept them, and 60+ known consent platforms, newsletter popups, and chat widgets are removed before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, with no card required.

FAQ

Should every Cypress test be independent?

Yes. Each test should establish its own prerequisites so it can run alone and still pass. This makes failures easier to reproduce and avoids order-dependent results.

Does Cypress clear IndexedDB between end-to-end tests?

Not as part of the documented end-to-end isolation reset, which clears cookies, localStorage, and sessionStorage. Add explicit setup or cleanup if your app depends on IndexedDB or another storage mechanism.

Are test retries enabled by default?

No. Cypress test retries are off by default. Enable them deliberately and treat retry passes as signals to investigate.

When should I use visible text as a selector?

Use it when the text itself matters to the test, such as verifying a label or message. For locating general interaction targets, a dedicated data attribute is usually less sensitive to copy changes.

Sources