Cypress E2E Testing

Cypress is a popular end-to-end testing tool built around developer ergonomics: it runs in the browser alongside your app, automatically waits for elements, and offers time-travel debugging where you can step back through each command and inspect the DOM at that moment. The result is a tight, visual feedback loop that many developers find friendlier than out-of-process runners.

Like all E2E tooling, Cypress belongs at the top of the pyramid — a small, stable suite for critical user flows, with most logic verified by faster unit and integration tests. Understanding its command-queue model is the key to writing tests that aren't flaky.

TL;DR

Quick Example

Commands read like a user's actions and auto-wait at each step:

Setup

The Command-Queue Model

Cypress feels different because of how it runs:

This is what makes tests read simply without explicit waits — but you need the model in mind: commands are asynchronous and chained, not imperative line-by-line code.

Selectors & Custom Commands

Network Stubbing

Stubbing (cy.intercept) helps when the backend is unstable, you want to test edge cases deterministically, or you want faster feedback. But don't stub everything — keep some real-backend tests to catch integration issues that stubs would hide.

Component Testing

Cypress can also test components in isolation, in a real browser — faster than full E2E while keeping real rendering. This complements unit tests rather than replacing them.

Best Practices

Common Mistakes

Order-dependent, shared-state tests

Stubbing everything

FAQ

Cypress or Playwright?

Cypress runs in-browser with a polished interactive runner and time-travel debugging that's great for developer experience and debugging single-browser web apps. Playwright runs out-of-process, supports Chromium/Firefox/WebKit with true parallelism, and has first-class tracing — stronger for cross-browser coverage and large CI suites. Choose Cypress for DX and in-browser debugging, Playwright for breadth and scale.

Why do my Cypress commands behave asynchronously?

Because Cypress queues commands and runs them asynchronously with automatic retries — cy.get(...) doesn't return an element synchronously; it enqueues work. That's why you chain with .then() and assertions rather than assigning results to variables. Fighting the queue (mixing in synchronous logic that expects immediate values) is a common source of confusion and bugs.

How do I keep Cypress tests from being flaky?

Use stable data-testid selectors, rely on Cypress's built-in retry/auto-wait (avoid cy.wait(ms) with fixed times — wait on a request or assertion instead), seed and reset data so tests are independent, and don't over-stub. Most flakiness comes from timing assumptions, shared state, or test-order dependencies — remove those.

Should I stub the network or hit a real backend?

Both, deliberately. Stub (cy.intercept) for speed, deterministic edge cases, and when the backend is unreliable. But keep a layer of tests against a real backend so genuine integration problems — serialization, auth, schema mismatches — surface. Stubbing everything gives fast but false confidence.

Related Topics

References