Skip to content
Codeloom
Testing

End-to-End Testing with Cypress

Write reliable E2E tests with Cypress — selectors, assertions, network stubbing, custom commands, and CI integration.

·4 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • How Cypress runs tests in a real browser
  • Selecting elements, asserting state, and handling async
  • Network stubbing and fixtures
  • Custom commands and CI integration

Prerequisites

  • JavaScript basics
  • A web application to test

Cypress runs end-to-end tests in a real browser. Unlike Selenium, it runs inside the browser alongside your application, giving it direct access to the DOM, network requests, and timers. Tests are fast, reliable, and easy to debug.

Setup

npm install -D cypress
npx cypress open

Cypress creates a cypress/ directory with example specs. Your tests go in cypress/e2e/.

Your first test

// cypress/e2e/login.cy.js
describe('Login', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('shows validation errors for empty form', () => {
    cy.get('[data-testid="submit"]').click();
    cy.get('[data-testid="error"]').should('be.visible');
    cy.get('[data-testid="error"]').should('contain', 'Email is required');
  });

  it('logs in with valid credentials', () => {
    cy.get('[data-testid="email"]').type('alice@example.com');
    cy.get('[data-testid="password"]').type('password123');
    cy.get('[data-testid="submit"]').click();

    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="welcome"]').should('contain', 'Alice');
  });
});

Selecting elements

Prefer data-testid attributes over CSS classes or tag names — they survive refactors.

// Best — dedicated test attribute
cy.get('[data-testid="submit-button"]');

// Acceptable — semantic selector
cy.get('button[type="submit"]');

// Avoid — brittle, tied to styling
cy.get('.btn-primary.large');

Finding within elements

cy.get('[data-testid="user-card"]')
  .first()
  .within(() => {
    cy.get('[data-testid="name"]').should('have.text', 'Alice');
    cy.get('[data-testid="role"]').should('have.text', 'Admin');
  });

Assertions

Cypress uses Chai assertions and retries automatically until they pass (up to the timeout).

// Visibility
cy.get('.modal').should('be.visible');
cy.get('.modal').should('not.exist');

// Text content
cy.get('h1').should('have.text', 'Dashboard');
cy.get('p').should('contain', 'Welcome');

// Attributes
cy.get('input').should('have.attr', 'placeholder', 'Search...');
cy.get('button').should('be.disabled');

// CSS
cy.get('.alert').should('have.css', 'color', 'rgb(255, 0, 0)');

// Length
cy.get('li').should('have.length', 5);

Interacting with elements

// Typing
cy.get('input').type('hello world');
cy.get('input').clear().type('new value');

// Clicking
cy.get('button').click();
cy.get('.menu-item').first().click();

// Selecting
cy.get('select').select('Option 2');

// Checkboxes
cy.get('[type="checkbox"]').check();
cy.get('[type="checkbox"]').uncheck();

// File upload
cy.get('input[type="file"]').selectFile('cypress/fixtures/photo.jpg');

Network stubbing with intercept

Stub API responses to isolate frontend tests from backend.

describe('User list', () => {
  beforeEach(() => {
    cy.intercept('GET', '/api/users', {
      statusCode: 200,
      body: [
        { id: 1, name: 'Alice', role: 'admin' },
        { id: 2, name: 'Bob', role: 'user' },
      ],
    }).as('getUsers');

    cy.visit('/users');
    cy.wait('@getUsers');
  });

  it('displays users from the API', () => {
    cy.get('[data-testid="user-row"]').should('have.length', 2);
    cy.get('[data-testid="user-row"]').first().should('contain', 'Alice');
  });
});

Waiting for real requests

cy.intercept('POST', '/api/orders').as('createOrder');

cy.get('[data-testid="place-order"]').click();
cy.wait('@createOrder').its('response.statusCode').should('eq', 201);

Fixtures

Store test data in cypress/fixtures/:

// cypress/fixtures/users.json
[
  { "id": 1, "name": "Alice" },
  { "id": 2, "name": "Bob" }
]
cy.intercept('GET', '/api/users', { fixture: 'users.json' });

Custom commands

// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login');
  cy.get('[data-testid="email"]').type(email);
  cy.get('[data-testid="password"]').type(password);
  cy.get('[data-testid="submit"]').click();
  cy.url().should('include', '/dashboard');
});

Use in tests:

beforeEach(() => {
  cy.login('alice@example.com', 'password123');
});

API login (faster)

Cypress.Commands.add('loginViaAPI', (email, password) => {
  cy.request('POST', '/api/login', { email, password }).then((resp) => {
    window.localStorage.setItem('token', resp.body.token);
  });
});

Page objects pattern

// cypress/pages/LoginPage.js
export const LoginPage = {
  visit: () => cy.visit('/login'),
  emailInput: () => cy.get('[data-testid="email"]'),
  passwordInput: () => cy.get('[data-testid="password"]'),
  submitButton: () => cy.get('[data-testid="submit"]'),
  errorMessage: () => cy.get('[data-testid="error"]'),

  login(email, password) {
    this.emailInput().type(email);
    this.passwordInput().type(password);
    this.submitButton().click();
  },
};

CI integration

# .github/workflows/e2e.yml
name: E2E Tests
on: [push]
jobs:
  cypress:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cypress-io/github-action@v6
        with:
          build: npm run build
          start: npm start
          wait-on: 'http://localhost:3000'

Configuration

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 10000,
    video: false,
    screenshotOnRunFailure: true,
  },
});

Summary

Cypress gives you fast, reliable E2E tests that run in a real browser. Use data-testid for stable selectors, cy.intercept for network stubbing, custom commands for reusable flows, and the GitHub Action for CI. Start with your login flow and critical user paths — those are the tests that catch the most regressions.