Cypress has become the gold standard for E2E testing in modern web applications. Here’s a comprehensive guide to implementing effective end-to-end tests with Cypress:
Core Concepts
Why Cypress?
- All-in-one solution: Test runner, assertion library, and mocking built in
- Time travel: Debug tests with snapshots at each step
- Real-time reloads: See changes as you write tests
- Automatic waiting: No need for arbitrary timeouts
Basic Test Structure
describe('User Authentication', () => {
beforeEach(() => {
cy.visit('/login');
});
it('should log in with valid credentials', () => {
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="password"]').type('validPassword123');
cy.get('[data-testid="submit"]').click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome back, user@example.com');
});
});
Configuration
cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1920,
viewportHeight: 1080,
setupNodeEvents(on, config) {
// Configure plugins here
},
env: {
apiUrl: 'https://api.your-app.com'
}
}
});
Essential Testing Patterns
1. Element Selection Best Practices
// Good - semantic and stable selectors
cy.get('nav').find('a').contains('Home');
cy.get('[data-testid="login-button"]');
// Avoid - brittle selectors
cy.get('#root > div > div:nth-child(3) > button');
2. Form Testing
it('should submit registration form', () => {
cy.get('[data-testid="first-name"]').type('John');
cy.get('[data-testid="last-name"]').type('Doe');
cy.get('[data-testid="email"]').type('john.doe@example.com');
cy.get('[data-testid="terms"]').check();
cy.get('[data-testid="submit"]').click();
cy.url().should('include', '/welcome');
cy.contains('Thank you for registering, John!');
});
3. API Request Handling
// Mocking API responses
it('should display products from API', () => {
cy.intercept('GET', '/api/products', {
fixture: 'products.json'
}).as('getProducts');
cy.visit('/products');
cy.wait('@getProducts');
cy.get('[data-testid="product"]').should('have.length', 5);
});
// Testing actual API calls
it('should create new order', () => {
cy.intercept('POST', '/api/orders').as('createOrder');
cy.visit('/checkout');
// ... fill out form
cy.get('[data-testid="place-order"]').click();
cy.wait('@createOrder').then((interception) => {
expect(interception.response.statusCode).to.equal(201);
});
});
Advanced Techniques
1. Authentication Patterns
// Custom login command (add to cypress/support/commands.js)
Cypress.Commands.add('login', (email, password) => {
cy.session([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');
});
});
// Usage in tests
describe('Authenticated Tests', () => {
beforeEach(() => {
cy.login('user@example.com', 'password123');
cy.visit('/dashboard');
});
});
2. Visual Regression Testing
// With cypress-image-snapshot plugin
it('should match homepage snapshot', () => {
cy.visit('/');
cy.matchImageSnapshot('homepage');
});
3. Component Testing (Cypress 10+)
// cypress/component/Button.cy.js
import Button from '../../src/components/Button';
describe('Button Component', () => {
it('should render with correct text', () => {
cy.mount(<Button>Click me</Button>);
cy.contains('Click me').should('be.visible');
});
});
CI/CD Integration
1. GitHub Actions Example
name: E2E Tests
on: [push]
jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm install
- run: npm start &
- uses: cypress-io/github-action@v5
with:
start: npm run dev
wait-on: 'http://localhost:3000'
2. Parallelization
# Run tests across 3 machines
npx cypress run --record --key your-record-key --parallel --ci-build-id $BUILD_ID
Best Practices
- Test Isolation: Each test should be independent
- Atomic Tests: Test one thing per test case
- Page Objects: Create reusable component classes
- Conditional Testing: Use
cy.get().should()
instead of manual waits - Accessibility: Incorporate a11y checks
// Example Page Object
class LoginPage {
visit() {
cy.visit('/login');
}
fillEmail(email) {
cy.get('[data-testid="email"]').type(email);
}
// ... other methods
}
Debugging Tips
1. Time Travel
cy.get('button').click();
cy.pause(); // Pause test runner
cy.get('result').should('contain', 'Success');
2. Console Output
cy.get('user').then(($el) => {
console.log('Found user:', $el.text());
});
3. Screenshots on Failure
afterEach(() => {
if (Cypress.currentTest.state === 'failed') {
cy.screenshot();
}
});
Performance Optimization
1. Smart Waiting
// Instead of:
cy.wait(5000);
// Use:
cy.get('.loader', { timeout: 10000 }).should('not.exist');
2. API Mocking
// Mock heavy API responses to speed up tests
cy.intercept('GET', '/api/large-dataset', { fixture: 'small-mock.json' });
Cross-Browser Testing
// cypress.config.js
module.exports = defineConfig({
e2e: {
browsers: [
{
name: 'chrome',
family: 'chromium',
channel: 'stable'
},
{
name: 'firefox',
family: 'firefox'
}
]
}
});