Free Ebook cover Cypress End-to-End Testing Basics: Stable UI Tests for Web Apps

Cypress End-to-End Testing Basics: Stable UI Tests for Web Apps

New course

10 pages

Cypress End-to-End Testing Basics: What E2E Tests Cover and How Cypress Runs Them

Capítulo 1

Estimated reading time: 7 minutes

+ Exercise

1) What a stable E2E test should prove

End-to-end (E2E) UI tests for web apps validate that a real user can complete critical flows in a real browser, with the app running as it would in production-like conditions. The scope is intentionally user-visible: what the user can do and what the user can observe.

What Cypress is actually verifying

  • User-visible behavior: pages render, navigation works, forms accept input, validation messages appear, and success states are shown.
  • Critical flows: the smallest set of journeys that keep the business working (for example: sign in, search, add to cart, checkout, submit a contact form).
  • Integration across layers: UI + routing + API calls + state management, as experienced through the browser.

What “stable” means in practice

A stable E2E test proves outcomes, not mechanics. It should answer: “If a user performs these actions, do they get the expected result?” It should avoid depending on fragile details like DOM structure, CSS classes used for layout, or internal framework state.

  • Prefer asserting outcomes: URL changes, visible text, enabled/disabled state, presence of a confirmation message, item count updated.
  • Prefer interacting like a user: click buttons/links, type into inputs, select options.
  • Keep the scope tight: one test should validate one meaningful behavior; multiple behaviors should be split into separate tests.

2) Guided walkthrough of the Cypress runner UI and test output

Cypress runs your tests in a dedicated test runner that controls a real browser. Understanding what you see in the runner helps you debug failures and write more reliable assertions.

How Cypress executes a spec (practical model)

  • Spec files: your test files (for example, cypress/e2e/login.cy.js) contain describe()/it() blocks and Cypress commands like cy.visit().
  • Test runner: the UI that lists specs, runs them, and shows command logs and snapshots.
  • Browser context: tests execute inside a real browser (Chromium/Electron/Chrome/Firefox depending on your setup). The app is loaded as a user would load it, but Cypress injects hooks to observe and control the page.
  • Command queue: Cypress commands (anything starting with cy.) are enqueued and executed in order. They are not executed immediately when the line is reached; Cypress schedules them and runs them deterministically.
  • Assertions and retry-ability: assertions like should() automatically retry for a period of time until they pass or time out. This is key to stability: you typically do not need manual waits when you assert the right thing.

What you see in the runner UI

  • Spec list: pick a spec to run. This is useful for running one flow while developing.
  • Test results panel: shows each describe/it and whether it passed/failed.
  • Command log: a step-by-step list of Cypress commands (visit, get, click, type, should). Clicking a command shows details (selector used, yielded element, assertion result).
  • Snapshots/time travel: Cypress captures DOM snapshots at each command. When you click a command in the log, the app preview rewinds to that moment so you can see the state that produced the failure.
  • Errors with context: failures usually show the assertion that failed, what was expected vs what was found, and often the element Cypress was acting on.

How to read the output when a test fails (step-by-step)

  • Step 1: Identify which it() failed and read the failure message (expected vs actual).
  • Step 2: In the command log, click the last successful command, then the failing command, and compare the snapshots.
  • Step 3: Check whether the failure is about timing (element not yet visible) or about correctness (wrong text, wrong navigation).
  • Step 4: If timing-related, prefer adding a better assertion (for example, should('be.visible') on the element that indicates readiness) rather than adding arbitrary waits.

3) A minimal example test (visit, interact, assert)

The smallest useful E2E test has three parts: navigate to a page, perform a user action, and assert a user-visible outcome.

Example: login flow with a visible success indicator

describe('Login', () => {  it('lets a user sign in and see their dashboard', () => {    cy.visit('/login');    cy.get('[data-cy=email]').type('user@example.com');    cy.get('[data-cy=password]').type('correct-horse-battery-staple');    cy.get('[data-cy=submit]').click();    cy.url().should('include', '/dashboard');    cy.get('[data-cy=welcome]').should('contain.text', 'Welcome');  });});

What each line is proving

  • cy.visit('/login'): the login page can load.
  • cy.get(...).type(...): the user can interact with the form controls.
  • click(): the user can submit the form.
  • cy.url().should('include', '/dashboard'): the app navigates to the expected destination (a user-visible outcome).
  • should('contain.text', 'Welcome'): the destination page shows a meaningful indicator that the user is signed in.

Why this stays stable

  • It asserts on outcomes (URL + visible welcome area) rather than internal state.
  • It uses a dedicated, test-friendly selector strategy (data-cy) instead of brittle CSS or DOM traversal.
  • It relies on Cypress’ built-in retry behavior: should() will wait for the URL and the welcome element to match expectations.

4) Common misconceptions (testing implementation details, over-asserting)

Misconception: “E2E tests should verify everything on the page”

E2E tests are expensive compared to unit/component tests. Their job is to confirm that key user journeys work. If you assert every label, every CSS class, and every pixel-level detail, you create brittle tests that fail for harmless UI changes.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

  • Prefer: one or two assertions that prove the flow succeeded.
  • Avoid: asserting every field placeholder, every icon, or exact full-page text dumps.

Misconception: “If it’s in the DOM, it’s fair game to assert”

Many DOM details are not user-visible or are incidental to implementation (wrapper divs, framework-generated attributes, layout classes). Asserting on them couples your test to the current markup structure.

  • Prefer: visible text, ARIA roles/labels where appropriate, and stable test IDs.
  • Avoid: selectors like div > :nth-child(2) > .btn.primary or framework-specific attributes that may change.

Misconception: “Cypress commands run immediately like normal JavaScript”

Cypress commands are queued and executed later. This affects how you structure tests and why Cypress can provide time-travel debugging. It also explains why mixing synchronous code with Cypress commands can be confusing if you expect immediate values.

  • Prefer: chaining Cypress commands and assertions (for example, cy.get(...).should(...)).
  • Avoid: trying to store values from Cypress commands in plain variables and using them synchronously without Cypress’ chaining mechanisms.

Misconception: “Adding waits makes tests stable”

Hard waits (for example, waiting 2 seconds) often make tests slower and still flaky across machines and network conditions. Cypress is designed around retrying commands and assertions until the UI reaches the expected state.

  • Prefer: waiting on a meaningful condition via assertions (element visible, request finished, URL updated).
  • Avoid: arbitrary sleeps to “let the page load.”

Checklist: what to validate vs what to avoid

Validate (good E2E scope)

  • Critical user journeys complete end-to-end (navigation, form submission, confirmation).
  • Key UI states are visible: success messages, error messages, empty states, loading-to-ready transitions.
  • Routing outcomes: URL changes, protected routes require auth, redirects happen as expected.
  • Data-impacting actions show the expected result in the UI (item added, profile updated, order created).
  • Accessibility-relevant behavior at a basic level: interactive elements are reachable and produce visible outcomes (when your app uses accessible labels/roles).

Avoid (common sources of flakiness and low value)

  • Asserting on internal implementation details (framework state, private APIs, component internals).
  • Brittle selectors (deep CSS paths, :nth-child, layout classes).
  • Over-asserting cosmetic details (exact copy everywhere, pixel-perfect layout, non-critical icons).
  • Hard waits instead of condition-based assertions.
  • Combining too many behaviors into one long test that is hard to diagnose when it fails.

Now answer the exercise about the content:

Which approach best matches how to make Cypress E2E UI tests stable?

You are right! Congratulations, now go to the next page

You missed! Try again.

Stable E2E tests prove outcomes a user can observe (e.g., URL and visible indicators) and avoid brittle implementation details. Cypress assertions like should() retry, so condition-based assertions are preferred over hard waits.

Next chapter

Setting Up a Cypress Project for Reliable Web App Testing

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.