End-to-End Testing with Cypress
Write reliable E2E tests with Cypress — selectors, assertions, network stubbing, custom commands, and CI integration.
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.
Related articles
- Testing Integration Tests vs Unit Tests: Finding the Right Balance
Understand the tradeoffs between unit tests and integration tests. Learn the testing pyramid, trophy, and practical strategies for effective test suites.
- Testing Mocking Strategies for Unit Tests
Learn when and how to mock dependencies in unit tests. Covers stubs, spies, fakes, mock patterns, and how to avoid over-mocking.
- Testing pytest Fixtures: A Complete Guide
Master pytest fixtures for reusable test setup and teardown. Covers scopes, parameterization, factories, autouse, and fixture composition.
- Testing Testing React Components with Testing Library
Learn to test React components the right way with React Testing Library. Covers rendering, queries, user events, async testing, and best practices.