End-to-End Testing with Cypress

Loading

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

  1. Test Isolation: Each test should be independent
  2. Atomic Tests: Test one thing per test case
  3. Page Objects: Create reusable component classes
  4. Conditional Testing: Use cy.get().should() instead of manual waits
  5. 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'
      }
    ]
  }
});

Leave a Reply

Your email address will not be published. Required fields are marked *