End-to-End Testing

Cypress

Cypress runs tests inside the browser with an interactive runner, automatic retries and a time-travel debugger that makes end-to-end testing unusually approachable.

intermediate13 min readUpdated Sep 15, 2026
login.cy.js
js
// cypress/e2e/login.cy.js
describe("login", () => {
  it("signs the user in", () => {
    cy.visit("/login");

    cy.get('[data-cy="email"]').type("[email protected]");
    cy.get('[data-cy="password"]').type("secret");
    cy.get('[data-cy="submit"]').click();

    cy.contains("h1", "Dashboard").should("be.visible");
  });
});
Runs in
The browser
Retries
Automatic
Debugging
Time-travel runner
Network
cy.intercept
Language
JavaScript or TypeScript
Browsers
Chrome, Firefox, Edge, WebKit

Why it matters

Why developers like Cypress

Interactive runner

Watch commands execute against the live app, inspect the DOM at each step and time-travel to any point in the test.

Automatic retries

Commands and assertions retry until they pass or time out, which removes most manual waits.

Network stubbing

Intercept and mock requests with cy.intercept to test error states and edge cases deterministically.

The big picture

The three ideas behind Cypress

Tests run inside the browser, commands queue automatically, and assertions retry until they pass.

Commands

Act

A chainable API for visiting pages, querying elements and interacting with them.

Assertions

Verify

should and expect assertions that retry against the live DOM.

The runner

Execute

A Node process drives the browser and serves the interactive test runner.

Cypress at a glance

The core of Cypress

cy.visit

Open a page and begin a test.

cy.get and cy.contains

Query elements by selector, text or a testing-library style helper.

should

Assertions that retry until the condition is true.

Interactions

type, click, select, trigger and more, all queued and retried.

cy.intercept

Stub, spy on and modify network requests and responses.

Custom commands

Encapsulate repeated flows into reusable commands.

A short history

From a dev tool to a testing platform

  1. 2015

    Cypress released

    A browser-based testing tool with an interactive runner is introduced.

    15
  2. 2018

    Cypress 3 and open source

    Wider adoption and a growing plugin ecosystem follow the open-source release.

    18
  3. 2020

    cy.intercept

    A new network API replaces cy.route and improves stubbing and spying.

    20
  4. 2022

    Cypress 10 and 12

    A new configuration format and improved component testing ship.

    22
  5. Today

    A popular e2e choice

    Loved for its developer experience, with component testing and cloud reporting.

    Today

The complete guide

Cypress: Everything you need to know

What is Cypress?

Cypress is an end-to-end testing tool that runs inside the browser alongside your application. That architectural choice gives it an unusually good developer experience: an interactive runner where you watch commands execute, the ability to inspect the DOM at every step, and a time-travel debugger that replays each action.

It is one of the most popular browser-testing tools and is particularly loved for its approachability. Commands queue automatically, assertions retry until they pass, and the runner makes it obvious what happened when a test fails.

Writing a test

Cypress tests use a chainable command API and a describe/it structure.

// cypress/e2e/todos.cy.js
describe("todos", () => {
  beforeEach(() => {
    cy.visit("/todos");
  });

  it("adds a todo", () => {
    cy.get('[data-cy="new-todo"]').type("Write tests");
    cy.get('[data-cy="add"]').click();

    cy.get('[data-cy="todo"]').should("have.length", 1);
    cy.contains("Write tests").should("be.visible");
  });
});

Cypress commands are queued and run in order, even though they return immediately. That is why you chain .then or use aliases instead of assigning a command’s result to a variable.

Commands and assertions

The core commands cover navigation, queries and interaction:

  • cy.visit(url) opens a page.
  • cy.get(selector) queries by CSS selector.
  • cy.contains(text) finds an element by its text.
  • .type(), .click(), .select(), .check() interact with elements.
  • .should() asserts and retries.
// assertions.cy.js
cy.get("form").should("be.visible");
cy.get('[data-cy="count"]').should("have.text", "3");
cy.get("button").should("be.disabled");
cy.get('[data-cy="row"]').should("have.length", 5);

Because .should() retries until it passes or times out, you rarely need to wait explicitly. This is the same retry-based approach that makes Playwright tests resilient, applied to a chained API.

Network stubbing with cy.intercept

cy.intercept is how you make tests deterministic. It can stub responses, spy on real traffic, modify requests and control timing.

// intercept.cy.js
cy.intercept("GET", "/api/users", {
  statusCode: 500,
  body: { message: "Server error" },
}).as("users");

cy.visit("/users");
cy.wait("@users");

cy.contains("Something went wrong").should("be.visible");

Using an alias with cy.wait("@users") lets you assert on the request and wait for the response without guessing at timing. Stubbing error states this way is far more reliable than trying to provoke them from a real backend.

Custom commands and fixtures

Repeated flows belong in a custom command, which keeps tests readable.

// cypress/support/commands.js
Cypress.Commands.add("login", (email, password) => {
  cy.visit("/login");
  cy.get('[data-cy="email"]').type(email);
  cy.get('[data-cy="password"]').type(password);
  cy.get('[data-cy="submit"]').click();
});
// usage
cy.login("[email protected]", "secret");

Fixtures load static data from cypress/fixtures, and cy.fixture("users.json").then(...) gives tests predictable inputs. Combined with cy.intercept, fixtures are the standard way to test without a live backend.

Selecting elements

Cypress works with CSS selectors, so the temptation to bind tests to styling is real. Avoid it. Prefer, in order:

  1. Accessible queries via @testing-library/cypress (findByRole, findByLabelText).
  2. cy.contains for visible text.
  3. A dedicated data-cy attribute when there is no user-facing identity.

A data-cy attribute is explicit and stable, so tests survive redesigns. Class-based selectors do not.

Where Cypress fits

Cypress is the end-to-end layer. Keep the pyramid balanced: fast unit tests with Vitest or Jest, component tests with Testing Library, and a focused set of end-to-end journeys in Cypress or Playwright. Push edge cases down to the cheaper layers and reserve e2e for the paths that must work end to end.

Best practices

  • Stub the network with cy.intercept for deterministic tests.
  • Use data-cy or accessible queries instead of styling selectors.
  • Keep tests independent so they can run in parallel.
  • Encapsulate repeated flows in custom commands.
  • Log in through a custom command or API call rather than the UI every time.
  • Assert on user-visible outcomes, not internal state.
  • Run headless in CI and keep the interactive runner for local debugging.

Common mistakes

  • Waiting with cy.wait(milliseconds) instead of on a network alias or an assertion.
  • Coupling selectors to CSS classes and structure.
  • Sharing state between tests and causing order-dependent failures.
  • Trying to store command results in variables without .then or aliases.
  • Testing everything end-to-end instead of at the right layer.
  • Logging in through the UI before every test and slowing the suite down.

Where to go next

Cypress is a friendly, powerful way to test real user journeys. Compare it with Playwright for the cross-browser alternative, and build the lower layers with Vitest and Testing Library. Then add a custom login command and one critical flow, and grow the suite from there.

Handling the network

Stub the request so the test is deterministic and fast. Waiting on a real backend makes tests slow and flaky.

Prefer
cy.intercept("GET", "/api/users", {
  statusCode: 500,
  body: { message: "Server error" },
}).as("users");

cy.visit("/users");
cy.wait("@users");
cy.contains("Something went wrong");
Avoid
cy.visit("/users");
cy.wait(3000);
cy.contains("Something went wrong");

Selecting elements

A dedicated test attribute is stable across styling and structure changes.

Prefer
cy.get('[data-cy="submit"]').click();
Avoid
cy.get(".btn.btn-primary > span")
  .click();

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Cypress?

Our interactive tutorial walks you through Cypress step by step — with quizzes and real code you can run in the browser.