Writing Reliable Cypress Tests
Cypress tests fail for two very different reasons: the application genuinely broke, or the test itself is poorly written and reports a false negative. Best practices exist to eliminate the second category so that every red run in CI actually means something is wrong with the product. The discipline is less about learning new commands and more about consistently applying a small set of rules around selectors, waiting, and test isolation across an entire suite.
Cricket analogy: Just as a third umpire only overturns a decision when there is conclusive replay evidence, a good test suite should only turn red when there is conclusive proof the app broke, not because the test itself mistimed a DRS-style check.
Selecting Elements Robustly
Cypress recommends targeting elements with dedicated test attributes such as data-cy, data-test, or data-testid rather than CSS classes, tag names, or text content. Classes and styles change frequently as designers iterate on the UI, and text content changes with copy edits or localization, both of which break tests that have nothing to do with actual functional regressions. A data-cy attribute is a contract between the test suite and the markup that survives refactors of styling and wording alike.
Cricket analogy: A team that tags a fielder by shirt number rather than by 'the guy near point' can reposition players for different formations without ever losing track of who is who, just as data-cy attributes survive layout changes that would break a CSS-class selector.
Eliminating Flakiness with Retry-ability
Cypress commands like cy.get and cy.contains automatically retry until an element exists and satisfies the assertion chained to it, or until the default 4-second timeout expires. This retry-ability means you almost never need cy.wait(3000)-style arbitrary sleeps; instead you assert on the condition you actually care about, such as an element being visible or containing specific text, and let Cypress poll for it. Arbitrary waits either fail intermittently on slow CI runners or waste time on fast ones, whereas condition-based waiting adapts automatically to real network and rendering latency.
Cricket analogy: A fielder who keeps his eye on the ball and adjusts his position continuously until the catch is safe is far more reliable than one who commits to a fixed spot after a set count, just as Cypress's retry loop adapts rather than committing to a fixed sleep.
// Bad: brittle selector and arbitrary wait
cy.get('.btn-primary').click();
cy.wait(3000);
cy.get('.success-msg').should('contain', 'Order placed');
// Good: stable selector, network-aware wait, retry-ability
cy.intercept('POST', '/api/orders').as('placeOrder');
cy.get('[data-cy=checkout-submit]').click();
cy.wait('@placeOrder').its('response.statusCode').should('eq', 201);
cy.get('[data-cy=order-confirmation]').should('contain', 'Order placed');Avoid cy.wait(<number>) in test code except as a last-resort debugging aid. It couples your test's timing to the current speed of your CI runner, causing tests that pass locally to flake under load. Prefer cy.intercept aliases (cy.wait('@alias')) or assertions that retry automatically.
Structuring Suites for Maintainability
As a suite grows past a handful of specs, repeated interaction sequences such as logging in, adding an item to a cart, or filling a multi-step form should be extracted into custom commands registered via Cypress.Commands.add. This keeps individual test files focused on the behavior being verified rather than on incidental setup steps, and it means a change to the login flow only requires updating one command instead of every spec file that logs in. Custom commands should still use the same stable data-cy selectors internally so the abstraction doesn't hide brittleness, it just centralizes it in one maintainable place.
Cricket analogy: A team that drills a specific fielding routine like the relay throw from the boundary so thoroughly that every player executes it identically is far more efficient than each player improvising their own version, just as a custom command standardizes a repeated interaction like login.
Cypress's cy.session() command caches authentication state (cookies, localStorage) across tests within a run, so a custom cy.login() command backed by cy.session only performs the real login once and restores the session instantly for subsequent tests, dramatically speeding up suites that need an authenticated user in every spec.
- Prefer dedicated data-cy/data-testid attributes over CSS classes or text content for element selection.
- Lean on Cypress's built-in retry-ability instead of cy.wait(<number>) to avoid flaky, timing-dependent tests.
- Use cy.intercept with aliases to wait on real network responses rather than guessing durations.
- Extract repeated interaction sequences into custom commands via Cypress.Commands.add.
- Use cy.session() to cache login state and speed up suites that need authentication in every test.
- Keep each test independent by resetting application state (via API calls or seed scripts) rather than depending on execution order.
- Treat every flaky test as a bug to fix immediately, not something to retry until it passes.
Practice what you learned
1. Why does Cypress recommend using data-cy attributes over CSS class selectors?
2. What is the main problem with using cy.wait(3000) in place of assertion-based waiting?
3. What does cy.session() primarily help with?
4. What is the purpose of extracting a login flow into a custom Cypress command?
5. Why should Cypress tests generally reset application state rather than rely on execution order?
Was this page helpful?
You May Also Like
Cypress Quick Reference
A condensed reference of the most-used Cypress commands, assertions, network mocking patterns, and configuration options for day-to-day test writing.
Building an E2E Test Suite for a Real App
A step-by-step approach to designing, implementing, and running a Cypress end-to-end test suite for a realistic e-commerce-style application.
Cypress Interview Questions
A curated set of commonly asked Cypress interview questions spanning fundamentals, architecture, and real-world troubleshooting scenarios.