Blog
Cypress End-to-End Testing: A Practical Guide for QA
A practical Cypress end-to-end testing guide for QA teams: architecture, selectors, assertions, network stubbing, flake prevention, CI, and honest tradeoffs versus Playwright.
- cypress
- e2e
- test-automation
- javascript
- qa
Cypress is a JavaScript end-to-end runner that many QA and frontend-heavy teams adopt because the local debugging experience is strong and the API encourages readable user-flow tests. This guide focuses on how Cypress actually behaves in CI and how to keep suites maintainable—not on collecting every plugin name.
For strategy context, see the automation testing guide. For a Playwright-oriented counterpart, see the Playwright testing tutorial.
Cypress architecture in plain terms
Cypress runs in the same run-loop as your application in the browser (with a Node process orchestrating). Practically, that means:
- Commands are queued and executed with automatic retries on many assertions and DOM queries.
- Time-travel snapshots in the interactive runner help you see each step’s DOM.
- Network traffic can be stubbed or waited on through
cy.intercept. - The model historically centered on a single tab inside Chromium-family browsers; multi-tab and multi-origin flows need careful patterns and current Cypress features.
You are not sending WebDriver protocol commands from a remote language binding the way classic Selenium does. That difference explains both Cypress’s excellent “watch the test run” DX and some of its limitations around multiple native windows or certain cross-origin setups.
When Cypress fits—and when it does not
Good fit
- App under test is a web UI your team already builds with JS/TS tooling
- Engineers want interactive debugging while authoring
- Most critical journeys stay in one origin/tab
- You want first-class network stubbing for front-end contract tests
Weaker fit
- You must exercise Safari/WebKit parity as a hard gate (evaluate current Cypress browser support for your versions before committing)
- Flows depend on many native OS dialogs, multiple real windows, or complex desktop-like behavior
- Your org standardizes on Java/Python automation with an existing Selenium grid and no Node appetite
Neither “Cypress is dead” nor “Cypress always wins” is useful advice. Match the runner to constraints.
Setup
npm install -D cypress
npx cypress opencypress open scaffolds a project and launches the interactive runner. For CI:
npx cypress runA minimal cypress.config.js:
const { defineConfig } = require("cypress");
module.exports = defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
specPattern: "cypress/e2e/**/*.cy.js",
supportFile: "cypress/support/e2e.js",
video: true,
screenshotOnRunFailure: true,
retries: {
runMode: 2,
openMode: 0,
},
},
});Put secrets and environment URLs in Cypress env config or CI variables—not in committed specs.
Your first test
Create cypress/e2e/login.cy.js:
describe("Login", () => {
it("valid user reaches dashboard", () => {
cy.visit("/login");
cy.get('[data-testid="email"]').type("qa.user@example.com");
cy.get('[data-testid="password"]').type("CorrectHorse-Battery1");
cy.get('[data-testid="submit"]').click();
cy.url().should("include", "/dashboard");
cy.contains("h1", "Dashboard").should("be.visible");
});
});Run headed for authoring (npx cypress open) and headless for pipelines (npx cypress run). Name tests after behavior the same way you title entries in a QA test case template.
Selectors that stay stable
Cypress encourages cy.get with CSS selectors. Stability still depends on what you select.
Prefer:
cy.get('[data-testid="checkout-submit"]');
cy.contains("button", "Place order");
cy.get("nav").contains("Orders");Avoid:
cy.get("div.container > div:nth-child(3) button");
cy.get(".css-1a2b3c"); // generated class hashescy.contains is powerful and dangerous: scope it. Prefer roles and test IDs agreed with frontend. When you introduce page objects or custom commands, keep them thin—see page object model for structure without burying the Cypress command queue in cleverness.
Custom command example for login:
// cypress/support/commands.js
Cypress.Commands.add("loginByUi", (email, password) => {
cy.visit("/login");
cy.get('[data-testid="email"]').type(email);
cy.get('[data-testid="password"]').type(password, { log: false });
cy.get('[data-testid="submit"]').click();
cy.url().should("include", "/dashboard");
});Prefer API login or session caching (cy.session) when UI login is slow or rate-limited, and keep a few UI login tests for the auth surface itself.
Assertions and the command queue
Cypress assertions use Chai-style .should / .and and retry until timeout:
cy.get('[data-testid="toast"]')
.should("be.visible")
.and("contain", "Saved");
cy.get('[data-testid="submit"]').should("be.disabled");Mental model that prevents confusion:
- Cypress commands are asynchronous but chained; you rarely need raw
async/awaitin classic Cypress tests. - Do not mix Cypress commands with non-Cypress async code carelessly—race conditions follow.
- Assertions retry; arbitrary JavaScript you run in
.thendoes not automatically retry unless you structure it correctly.
Timeouts belong in config or per-command options. Raising the global default to hide slow selectors creates long red builds instead of clear failures.
Network stubbing with cy.intercept
cy.intercept lets you observe or stub HTTP calls:
cy.intercept("GET", "/api/orders*", {
statusCode: 200,
body: {
orders: [{ id: "ord_1", totalCents: 2500, status: "open" }],
},
}).as("listOrders");
cy.visit("/orders");
cy.wait("@listOrders");
cy.contains("ord_1").should("be.visible");Waiting on aliases is preferable to fixed sleeps after navigation.
Stubbing a failure path:
cy.intercept("POST", "/api/checkout", {
statusCode: 502,
body: { message: "Upstream unavailable" },
}).as("checkoutFail");
cy.visit("/checkout");
cy.get('[data-testid="place-order"]').click();
cy.wait("@checkoutFail");
cy.contains("Try again").should("be.visible");Real backend vs mocks: choose deliberately
| Approach | Strength | Risk |
|---|---|---|
| Stubbed UI tests | Fast, deterministic UI states | Can drift from real API contracts |
| Real backend E2E | True integration confidence | Data collisions, env instability |
| Mixed | Stub edge failures; hit real happy path | Requires discipline so stubs do not silently dominate |
A healthy pattern: use real environments for a thin smoke set (see smoke testing vs sanity testing), and use stubs for hard-to-reproduce error states and front-end-only regressions. Contract tests or API suites should own payload shape; Cypress stubs should not become the only place “truth” lives.
Flake prevention habits
Cypress retries help, but retries without triage create false confidence. Common Cypress-specific flake sources:
- Animations and overlapping elements—assert visibility/enabled, or disable animations in test builds
- Flaky selectors tied to timing of client-side routing
- Stub/real mix-ups (forgot to remove a stub)
- Shared users in parallel
cypress runagainst one environment - Relying on wall-clock
cy.wait(5000)instead of route aliases or DOM conditions
Align with the general flaky tests playbook: isolate data, wait on conditions, keep smoke green, quarantine with visibility.
cy.session example for faster auth reuse:
cy.session(
email,
() => {
cy.visit("/login");
cy.get('[data-testid="email"]').type(email);
cy.get('[data-testid="password"]').type(password, { log: false });
cy.get('[data-testid="submit"]').click();
cy.url().should("include", "/dashboard");
},
{
validate() {
cy.request("/api/me").its("status").should("eq", 200);
},
}
);Cypress vs Playwright: tradeoffs, not marketing
| Topic | Cypress | Playwright |
|---|---|---|
| Authoring DX | Excellent interactive runner | Strong UI mode and trace viewer |
| Languages | JS/TS-first | JS/TS plus other official bindings |
| Browsers | Strong Chromium story; check current Safari/Firefox support for your needs | Chromium, Firefox, WebKit as first-class targets |
| Tabs / contexts | Improving, historically constrained | Multiple contexts and pages feel natural |
| Network control | cy.intercept is ergonomic | Route interception + APIRequestContext |
| Auto-waiting | Command retries and assertions | Actionability checks + web-first expect |
Choose Cypress when your team’s daily driver is JS and your journeys fit its browser model. Choose Playwright when cross-browser engines, multi-context flows, or mixed API+UI seeding are central. Switching frameworks mid-suite is expensive—fix waits and data first.
CI patterns
A typical job installs dependencies, starts the app, waits for a health URL, then runs cypress run. Artifact videos and screenshots on failure. For pipeline structure ideas, see GitHub Actions for test automation.
Principles that matter:
- Record video selectively if storage is costly; always keep failure screenshots
- Use
retries.runModemodestly; investigate systemic flake - Parallelization (Cypress Cloud or DIY sharding) needs isolated test data
- Keep PR smoke small; expand nightly
Example fragment:
- name: Cypress run
uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm run start
wait-on: "http://localhost:3000"
browser: chromeAdjust to your app’s start commands. Pin action versions and Node versions deliberately.
Organizing specs as the suite grows
Early Cypress projects dump everything into one folder. That works until the run exceeds a coffee break. Prefer risk-based grouping:
cypress/e2e/
smoke/
login.cy.js
checkout-happy.cy.js
regression/
orders/
settings/
edge/
checkout-errors.cy.jsTag or folder-filter smoke on pull requests; run regression on schedule. Mirror the same thinking you use for smoke testing vs sanity testing: protect the merge gate with a thin, trusted path.
Keep fixtures under cypress/fixtures/ for static JSON only when the data is truly static. Dynamic uniqueness (emails, order IDs) belongs in the spec or a factory helper so parallel runs do not collide.
const email = `qa+${Date.now()}@example.com`;Shared mutable users are a leading cause of intermittent auth and permission failures.
Testability partnerships with frontend
Cypress cannot fix an inaccessible, animation-heavy UI by itself. Agree on:
- Stable
data-testidvalues for critical controls - Deterministic test environments (seed scripts, feature-flag defaults)
- A “reduce motion” or animation-disable flag for test builds when transitions cause flake
- Documented deep links so tests do not click through five setup screens every time
When a control is an icon-only button, insist on an accessible name or test id. When a flow requires MFA, provide a test bypass in non-prod rather than automating SMS. These are product decisions; capture them as preconditions in your QA test case template so manual and automated runs stay honest.
From Jira story to Cypress spec
A maintainable path looks like this:
- Story acceptance criteria are numbered and reviewable
- Structured cases cover positive, negative, and edge paths
- Automation candidates are tagged (stable UI, high risk, cheap to assert)
- Cypress implements the tagged subset with clear names matching case titles
- Failures link back to case IDs in reports or PR comments
Skip step 2 and specs become a second, undocumented requirements language. QA Workflow Assistant helps with that drafting step by generating structured test cases from Jira stories; your team still reviews which ones deserve cy.intercept stubs versus a real backend path. Setup guidance lives in Docs.
Example of turning one case into a focused spec title:
- Case: “Buyer with valid cart completes checkout and sees order confirmation”
- Spec:
it("buyer completes checkout and sees order confirmation", ...)
Avoid packing five acceptance criteria into one it. When the assertion list grows past one user-visible outcome, split the test.
Debugging failures without guessing
Interactive mode (cypress open) is for authoring. In CI, lean on:
- Failure screenshots (default on)
- Videos for the failing spec (storage permitting)
cy.debug()and.pause()locally—not left in merged code- Command logs: which query retried until timeout?
Ask a fixed triage sequence: Did the route return the expected status? Was the element detached? Did a stub still apply from a previous experiment? Was the user data wrong? That sequence maps cleanly onto the flaky tests root-cause categories and keeps “just bump the timeout” from becoming culture.
Limitations to plan around
- Multi-tab / native window flows may need workarounds or a different tool.
- Some cross-origin journeys require
cy.originand careful session design—budget learning time. - Very large suites without tagging become slow; adopt smoke vs regression split early.
- Non-JS teams pay a language tax; training cost is real.
- Over-mocking produces green CI and red production.
- Heavy reliance on
cy.wait(ms)recreates the sleep-driven flake you left Selenium to escape.
None of these are reasons to avoid Cypress outright. They are reasons to scope the suite honestly and invest in testability with frontend partners.
FAQ
Should QA own Cypress or should developers?
Shared ownership works best: QA designs coverage and risk; developers keep selectors and testability healthy. A QA test case template keeps manual and automated intent aligned.
Do we need page objects in Cypress?
Not on day one. Introduce helpers or page modules when duplication hurts. Avoid deep inheritance. See page object model.
How many retries are acceptable?
Enough to absorb rare infra blips; not enough to hide broken selectors. Track retry rate as a quality signal per the flaky tests guidance.
How do stories become Cypress specs?
Draft structured cases from acceptance criteria, mark automation candidates, then implement. QA Workflow Assistant generates structured test cases from Jira stories so the backlog is reviewable before anyone opens cypress open. Product setup is in Docs; plan differences are on Pricing.
Closing
Cypress shines when you write focused journeys, stable selectors, intentional stubs, and a CI job that publishes failure evidence. Keep the suite curated—smoke on every PR, deeper coverage on a schedule—and keep case design sharp. When Jira stories move faster than drafting time, generate structured cases with QA Workflow Assistant, review them, and automate only the paths that deserve a browser.
Stay ahead in QA
Get practical QA guides, Jira & Xray tutorials, testing checklists, AI testing insights, and occasional product updates.
Related articles
Playwright Testing Tutorial: End-to-End Tests QA Teams Actually Maintain
A practical Playwright testing tutorial for QA and SDET teams: TypeScript setup, locators, auto-waiting, fixtures, API plus UI flows, traces, and CI patterns that reduce flake.
August 7, 2026 · 10 min read
API Test Automation: From Cases to Reliable CI Suites
How to turn API test cases into maintainable automated suites—framework structure, tooling, auth, data, contracts, parallelism, CI reporting, and Xray mapping.
August 7, 2026 · 13 min read
Automation Testing Guide for QA Engineers: Strategy, Frameworks, and CI
A practical automation testing guide for QA and SDET teams: what to automate, how to choose frameworks, structure suites, and wire CI without drowning in flaky UI noise.
August 7, 2026 · 16 min read