E2E Testing with Cypress

Unit tests cannot catch a login button that does nothing. This guide teaches Cypress end-to-end testing — driving a real browser through critical user journeys, selecting elements that survive redesigns, stubbing APIs, and running the suite in CI.

Written by Projectech7 min readPublished
For B.E./B.Tech Computer Science and IT students with working web apps who want automated tests that verify the whole app in a real browser like a user would Topics: Cypress, JavaScript, E2E Testing, QA
Illustration of end-to-end testing with Cypress showing an automated test driving a browser through a login flow with passing checks.
Illustration generated for this guide.
In this guide

Your unit tests pass. Your API returns the right JSON. And yet the login button does nothing when clicked, because the frontend sends username while the backend expects email. Unit tests cannot catch this — each side was tested in isolation, and both were "correct."

End-to-end (E2E) tests catch it: they drive a real browser, click real buttons, and verify what a real user would see. Cypress is the most approachable E2E tool for students. This guide shows you how to write tests that actually protect your app.

Where E2E fits in the testing pyramid

Layer What it tests Speed Example tool
Unit Single functions Milliseconds Jest
Integration Components working together Seconds Jest, Supertest
E2E Whole app in a real browser Tens of seconds Cypress, Playwright

The pyramid shape matters: many unit tests, fewer integration tests, few E2E tests. E2E tests are the slowest and most brittle, so you reserve them for critical user journeys — the flows where failure means real damage: signup, login, checkout, submitting the project form.

Note: E2E tests verify behavior through the UI. They are not a substitute for unit tests (too slow, too coarse) — they are the final safety net above them. See Testing JavaScript with Jest for the base of the pyramid.

Your first Cypress test

Install Cypress and open its interactive runner:

npm install --save-dev cypress
npx cypress open

The interactive runner is Cypress's killer feature for learning: you watch the test execute in a real browser, inspect each step, and time-travel through what happened. Write your first spec in cypress/e2e/login.cy.js:

describe('login flow', () => {
  it('logs in with valid credentials', () => {
    cy.visit('/login');
    cy.get('[data-cy=email]').type('student@example.com');
    cy.get('[data-cy=password]').type('correct-password');
    cy.get('[data-cy=submit]').click();
    cy.url().should('include', '/dashboard');
    cy.contains('Welcome back').should('be.visible');
  });

  it('shows an error with wrong password', () => {
    cy.visit('/login');
    cy.get('[data-cy=email]').type('student@example.com');
    cy.get('[data-cy=password]').type('wrong-password');
    cy.get('[data-cy=submit]').click();
    cy.contains('Invalid credentials').should('be.visible');
  });
});

Read it like a user story: visit, type, click, verify. cy.get() finds elements, .type() and .click() interact, .should() asserts. Cypress automatically retries assertions until they pass or time out — no manual sleep() calls waiting for the page.

Select with data-cy, not CSS classes

Notice [data-cy=email] instead of .login-input or #email-field. Dedicated data-cy attributes are the professional convention:

  • CSS classes change during redesigns; data-cy attributes exist only for tests
  • Text content changes with copy edits; data-cy stays stable
  • Tests break only when behavior actually changes — not when a designer renames a class

Add them to the handful of elements your tests touch. It takes seconds and saves hours of brittle-test maintenance.

Controlling the backend: cy.intercept

E2E tests hitting a real backend are slow and flaky. cy.intercept() lets you stub network responses:

it('shows the user dashboard', () => {
  cy.intercept('GET', '/api/profile', {
    statusCode: 200,
    body: { name: 'Test Student', projects: 3 },
  }).as('getProfile');

  cy.visit('/dashboard');
  cy.wait('@getProfile');
  cy.contains('Test Student').should('be.visible');
});

This tests your frontend against a known backend response — deterministic and fast. The judgment call: stub for frontend-logic tests; use the real backend (with a seeded test database) for the few critical journeys where you want true end-to-end confidence.

Test data and isolation

E2E tests must not depend on whatever data happens to be in the database:

  • Seed before, clean after. A beforeEach hook that resets the test database (via an API endpoint or direct DB seed) keeps tests independent.
  • Never test against production data. Use a dedicated test environment with its own database.
  • Unique data per run. Timestamps or random suffixes in test emails/usernames prevent collisions when tests run in parallel.

A test that passes only because yesterday's test created the right user is worse than no test — it fails mysteriously and teaches you to ignore failures.

Running in CI

Cypress runs headlessly in CI — no visible browser needed:

- name: Run Cypress E2E tests
  uses: cypress-io/github-action@v6
  with:
    start: npm start
    wait-on: 'http://localhost:3000'

The official GitHub Action starts your app, waits for it to respond, then runs the suite. Run E2E in CI on pull requests (not necessarily every commit — they are slow). Failures block the merge, which is exactly the protection you want before demo day. Pair this with the pipeline setup in the GitHub Actions guide.

What to test E2E (short list)

  • Authentication: login, logout, protected routes redirecting
  • The one core journey your app exists for (placing an order, submitting a project, booking a slot)
  • Critical forms: validation errors show, successful submit confirms
  • Payment-adjacent flows (with sandbox/test mode — never real charges)

Everything else belongs in unit or integration tests. Ten focused E2E tests beat a hundred flaky ones.

Debugging failing tests

Cypress's debugging experience is genuinely good — use it:

  • Time travel. In the interactive runner, hover over each command in the left panel to see exactly what the page looked like at that moment. Most failures become obvious: the button was not there yet, the modal covered it, the text differs.
  • Automatic evidence. Every failed run captures screenshots; with video enabled you get a full recording. In CI, upload these as artifacts — debugging a failure you cannot see is guesswork.
  • The usual suspects when a test flakes: timing (fixed with proper assertions and network aliases, never sleeps), test isolation (leaked state from a previous test), and selectors (a data-cy attribute beats an nth-child selector every time).
  • cypress open vs cypress run. Develop and debug with open (interactive, watch it happen); run headlessly with run in CI. If a test passes in open but fails in run, suspect viewport size or timing differences — set an explicit viewport in config.

A failing E2E test is either a real bug (celebrate — the suite earned its keep) or a test bug (fix immediately — a suite nobody trusts is a suite nobody runs).

Common mistakes

  • Testing everything E2E. Slow suite → developers stop running it → suite rots. Respect the pyramid.
  • Using CSS selectors or text as hooks. One redesign breaks fifty tests. Use data-cy attributes.
  • Hard cy.wait(5000) sleeps. They make suites slow and still flaky. Cypress's automatic retrying plus cy.wait('@alias') for network calls is the correct pattern.
  • Shared, unseeded test data. Tests that depend on leftover state fail randomly and erode trust in the suite.
  • Testing third-party widgets deeply. Your tests should verify your integration point (the payment button triggers checkout), not re-test Stripe's UI.
  • Ignoring flaky tests. A test that fails "sometimes" is a bug in the test or the app — investigate immediately. Teams that tolerate flakes end up ignoring the whole suite.

Quick checklist

  • Cypress installed; data-cy attributes on tested elements
  • Critical journeys covered: auth, core flow, key forms
  • Backend stubbed with cy.intercept where appropriate; real seeded DB for true E2E paths
  • Test database reseeded before each run; no production data touched
  • No hard sleeps; network waits via aliases
  • Suite runs in CI on pull requests and gates merging

Where to go from here

More project guides

More in Web Development