Comprehensive Guide to E2E Testing with Cypress

Published: March 28, 2025Reading time: 15 min

Introduction to E2E Testing

End-to-end (E2E) testing is a methodology used to test the entire application flow from start to finish. It tests the integration of all components of an application and simulates real user scenarios to ensure the application works correctly from the user's perspective.

Unlike unit tests that focus on individual functions or components, E2E tests verify that all parts of your application work together as expected. This includes frontend UI, API calls, database interactions, and any other external services your application depends on.

Cypress is a powerful JavaScript-based end-to-end testing framework designed specifically for modern web applications. It runs directly in the browser, giving it access to the entire DOM and application, which makes it ideal for testing web applications thoroughly.

Why Choose Cypress?

Time Travel Debugging

Cypress takes snapshots as your tests run, allowing you to see exactly what happened at each step.

Real-Time Reloads

Cypress automatically reloads when you make changes to your tests, speeding up your development workflow.

Automatic Waiting

Cypress automatically waits for commands and assertions before moving on, eliminating the need for explicit waits.

Network Traffic Control

Cypress can stub network requests, allowing you to control and test how your app behaves with different responses.

Cypress also provides a clean, intuitive syntax that makes tests easy to write and understand. Its comprehensive documentation and active community make it an excellent choice for teams implementing E2E testing.

Setting Up Cypress

Installation

To get started with Cypress, you need to install it as a development dependency in your project:

npm install cypress --save-dev

Or if you are using Yarn:

yarn add cypress --dev

Project Structure

After installing Cypress, you can open it for the first time to generate the default folder structure:

npx cypress open

This command will create a cypress directory in your project with the following structure:


                  cypress/
  ├── e2e/         # End-to-end test files
  ├── fixtures/     # Test data, mocks, and fixtures
  ├── support/      # Support files for tests
  │   ├── commands.js  # Custom Cypress commands
  │   └── e2e.js       # Imported in each end-to-end test file
  └── videos/       # Recorded test videos (when enabled)
                  

Configuration

Cypress creates a cypress.config.js file in your project root. This file allows you to customize various settings like the base URL, viewport size, timeout values, and more:

const { defineConfig } = require("cypress");

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 5000,
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
});

Writing Your First Cypress Test

Let us create a simple test to verify that our application's homepage loads correctly and contains the expected elements.

Create a new file in the cypress/e2e directory called homepage.cy.js:

// cypress/e2e/homepage.cy.js

describe('Homepage', () => {
  beforeEach(() => {
    // Visit the homepage before each test
    cy.visit('/');
  });

  it('should display the main heading', () => {
    cy.get('h1')
      .should('exist')
      .and('contain', 'Welcome to Our App');
  });

  it('should have a navigation menu', () => {
    cy.get('nav')
      .should('be.visible');
    
    cy.get('nav a')
      .should('have.length.at.least', 3);
  });

  it('should navigate to the about page', () => {
    cy.get('nav a')
      .contains('About')
      .click();
    
    cy.url()
      .should('include', '/about');
    
    cy.get('h1')
      .should('contain', 'About Us');
  });
});

Understanding Test Structure

  • describe: Groups related tests together
  • beforeEach: Runs before each test in the block, used for setup
  • it: Defines an individual test case
  • cy.visit: Navigates to a URL
  • cy.get: Selects elements on the page
  • should: Makes assertions about the selected elements

Running Tests

You can run your tests in two ways:

1. Interactive Mode:

npx cypress open

This opens the Cypress Test Runner, where you can select and run tests interactively.

2. Headless Mode:

npx cypress run

This runs all tests in headless mode, suitable for CI/CD pipelines.

Advanced Cypress Concepts

Custom Commands

Custom commands allow you to extend Cypress with your own functionality. They're perfect for encapsulating repeated tasks like login:

// cypress/support/commands.js

Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login');
  cy.get('input[name=email]').type(email);
  cy.get('input[name=password]').type(password);
  cy.get('button[type=submit]').click();
  
  // Verify login was successful
  cy.url().should('include', '/dashboard');
});

// Usage in a test
cy.login('user@example.com', 'password123');

Fixtures and Test Data

Fixtures let you store and reuse test data across tests:

// cypress/fixtures/users.json
{
  "admin": {
    "email": "admin@example.com",
    "password": "admin123"
  },
  "customer": {
    "email": "customer@example.com",
    "password": "customer123"
  }
}

// Usage in a test
cy.fixture('users').then((users) => {
  cy.login(users.admin.email, users.admin.password);
});

Network Interception

Cypress can intercept and modify network requests, allowing you to test how your app handles different API responses:

it('should display error message when API fails', () => {
  // Stub the API response
  cy.intercept('GET', '/api/products', {
    statusCode: 500,
    body: { error: 'Internal Server Error' }
  }).as('productsRequest');
  
  cy.visit('/products');
  cy.wait('@productsRequest');
  
  // Verify error message is displayed
  cy.get('.error-message')
    .should('be.visible')
    .and('contain', 'Failed to load products');
});

Testing Authentication

Testing authenticated areas often requires bypassing the UI login flow for efficiency:

// Programmatically log in without using the UI
beforeEach(() => {
  // Set auth token directly
  cy.window().then((window) => {
    window.localStorage.setItem('authToken', 'your-auth-token');
  });
  
  // Visit protected page
  cy.visit('/dashboard');
});

Best Practices for E2E Testing

Test Structure

  • Keep tests independent of each other
  • Follow the Arrange-Act-Assert pattern
  • Group related tests together

Selectors

  • Use data attributes for testing (e.g., data-testid="submit-button")
  • Avoid selecting elements by CSS classes that might change
  • Be specific but resilient in your selectors

Test Coverage

  • Focus on critical user flows first
  • Do not duplicate what unit tests already cover
  • Cover edge cases and error states

Performance

  • Be mindful of test execution time
  • Use cy.session() to preserve login state between tests
  • Stub network requests when possible

CI/CD Integration

Integrating Cypress tests into your CI/CD pipeline ensures tests are run automatically with every code change, catching issues early.

GitHub Actions Example

// .github/workflows/cypress.yml
name: Cypress Tests

on: [push, pull_request]

jobs:
  cypress-run:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
      
      - name: Install dependencies
        run: npm ci
      
      - name: Start application
        run: npm start & npx wait-on http://localhost:3000
      
      - name: Run Cypress tests
        uses: cypress-io/github-action@v5
        with:
          browser: chrome
          headed: false
      
      - name: Upload artifacts
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: cypress-screenshots
          path: cypress/screenshots

CircleCI Example


    // .circleci/config.yml
    version: 2.1

    orbs:
      cypress: cypress-io/cypress@3

    workflows:
      build-and-test:
        jobs:
          - cypress/run:
              start-command: 'npm start'
              wait-on: 'http://localhost:3000'
              store_artifacts: true
  

Real-World Testing Strategies

Testing Form Submissions

describe('Contact Form', () => {
  beforeEach(() => {
    cy.visit('/contact');
  });

  it('should submit successfully with valid data', () => {
    // Intercept form submission
    cy.intercept('POST', '/api/contact', {
      statusCode: 200,
      body: { message: 'Form submitted successfully' }
    }).as('formSubmit');
    
    // Fill out the form
    cy.get('input[name=name]').type('John Doe');
    cy.get('input[name=email]').type('john@example.com');
    cy.get('textarea[name=message]').type('This is a test message');
    cy.get('button[type=submit]').click();
    
    // Wait for submission and verify success
    cy.wait('@formSubmit');
    cy.get('.success-message')
      .should('be.visible')
      .and('contain', 'Thank you for your message');
  });

  it('should show validation errors for invalid data', () => {
    // Try to submit without filling required fields
    cy.get('button[type=submit]').click();
    
    // Verify error messages
    cy.get('.field-error')
      .should('have.length.at.least', 2)
      .first()
      .should('contain', 'This field is required');
  });
});

Testing User Workflows

describe('E-commerce Checkout Flow', () => {
  beforeEach(() => {
    // Setup: Login and add item to cart
    cy.login('customer@example.com', 'password');
    cy.visit('/products/1');
    cy.get('.add-to-cart-button').click();
    cy.get('.cart-icon').click();
  });

  it('should complete checkout process', () => {
    // Start checkout
    cy.get('.checkout-button').click();
    
    // Fill shipping details
    cy.get('input[name=address]').type('123 Test Street');
    cy.get('input[name=city]').type('Test City');
    cy.get('input[name=zipCode]').type('12345');
    cy.get('.continue-button').click();
    
    // Select payment method
    cy.get('input[value=credit-card]').check();
    cy.get('.continue-button').click();
    
    // Review order and submit
    cy.get('.order-summary').should('be.visible');
    cy.get('.place-order-button').click();
    
    // Verify order confirmation
    cy.url().should('include', '/order-confirmation');
    cy.get('.order-number').should('be.visible');
    cy.get('.thank-you-message').should('contain', 'Thank you for your order');
  });
});

Troubleshooting Common Issues

Elements Not Found

Problem: Cypress can not find elements that should exist on the page.

Solutions:

  • Check if your selectors are correct
  • Use cy.contains() to find text on the page
  • Increase command timeout in configuration
  • Check if elements are inside iframes (requires special handling)

Flaky Tests

Problem: Tests pass sometimes and fail other times with the same code.

Solutions:

  • Use explicit waits with cy.wait() for async operations
  • Add retry logic with cy.should()
  • Use more specific selectors
  • Check for race conditions in your app

CORS Errors

Problem: Cross-Origin Resource Sharing (CORS) issues when testing.

Solutions:

  • Configure your server to allow Cypress domain in CORS headers
  • Use cy.intercept() to stub responses
  • Run Cypress with --disable-web-security flag (not recommended for production)

Conclusion

End-to-end testing with Cypress offers a powerful way to ensure your application works correctly from the user's perspective. By simulating real user interactions and verifying expected behaviors, you can catch issues before they reach production.

While E2E tests may run slower than unit or integration tests, they provide a higher level of confidence in your application's functionality. A balanced testing strategy often includes all three types: many unit tests, some integration tests, and a few critical E2E tests.

As you continue your E2E testing journey, remember that the goal is not just to write tests, but to improve your application's quality and reliability for your users.

Femi Adigun profile picture

Femi Adigun

Senior Software Engineer & Coach

Updated March 28, 2024

Related Topics:
CypressE2E TestingJavaScriptWeb DevelopmentQA