Skip to content

E2E Testing Workflow

End-to-end (E2E) testing is a critical method for verifying overall application functionality. This guide covers how to use Claude Code to establish an efficient E2E testing workflow.

E2E Testing Overview

What is E2E Testing

E2E testing simulates real user operations to verify the complete flow from frontend to backend.

Value of E2E Testing

  • Verify user scenarios
  • Discover integration issues
  • Ensure functionality works
  • Increase release confidence
  • Reduce production issues

Testing Pyramid

       /\
      /E2E\      Few E2E tests
     /------\
    /Integration\  Moderate integration tests
   /----------\
  / Unit Tests  \  Many unit tests
 /--------------\

Using Claude to Write E2E Tests

Generate Test Cases

Have Claude generate E2E tests:

Write E2E tests for the user registration flow:

Flow:
1. Visit registration page
2. Fill form (username, email, password)
3. Submit form
4. Verify success message
5. Verify redirect to homepage

Using Cypress
Include:
- Happy path
- Form validation
- Error handling

Test Case Example

typescript
describe('User Registration', () => {
  beforeEach(() => {
    cy.visit('/register');
  });

  it('should successfully register a new user', () => {
    // Fill form
    cy.get('[data-testid="username"]').type('testuser');
    cy.get('[data-testid="email"]').type('test@example.com');
    cy.get('[data-testid="password"]').type('SecurePass123!');
    cy.get('[data-testid="confirm-password"]').type('SecurePass123!');

    // Submit form
    cy.get('[data-testid="submit"]').click();

    // Verify result
    cy.get('[data-testid="success-message"]')
      .should('be.visible')
      .and('contain', 'Registration successful');

    // Verify redirect
    cy.url().should('include', '/dashboard');
  });

  it('should validate required fields', () => {
    cy.get('[data-testid="submit"]').click();

    cy.get('[data-testid="username-error"]')
      .should('be.visible')
      .and('contain', 'Username is required');
  });

  it('should validate email format', () => {
    cy.get('[data-testid="email"]').type('invalid-email');
    cy.get('[data-testid="submit"]').click();

    cy.get('[data-testid="email-error"]')
      .should('be.visible')
      .and('contain', 'Invalid email format');
  });

  it('should validate password strength', () => {
    cy.get('[data-testid="password"]').type('weak');
    cy.get('[data-testid="submit"]').click();

    cy.get('[data-testid="password-error"]')
      .should('be.visible')
      .and('contain', 'Password is too weak');
  });

  it('should handle duplicate email', () => {
    // Use existing email
    cy.get('[data-testid="email"]').type('existing@example.com');
    cy.get('[data-testid="username"]').type('testuser');
    cy.get('[data-testid="password"]').type('SecurePass123!');
    cy.get('[data-testid="confirm-password"]').type('SecurePass123!');
    cy.get('[data-testid="submit"]').click();

    cy.get('[data-testid="error-message"]')
      .should('be.visible')
      .and('contain', 'Email already registered');
  });
});

Test Framework Selection

Cypress

Use Cypress for E2E testing:

Set up Cypress testing environment:

Install:
npm install --save-dev cypress

Configuration:
// cypress.config.js
export default {
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: false,
    screenshotOnRunFailure: true,
  },
};

Create tests:
cypress/e2e/user-flow.cy.js

Playwright

Use Playwright for E2E testing:

Set up Playwright testing environment:

Install:
npm install --save-dev @playwright/test

Configuration:
// playwright.config.js
export default {
  testDir: './tests',
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
};

Create tests:
tests/user-flow.spec.js

Puppeteer

Use Puppeteer for E2E testing:

Set up Puppeteer testing environment:

Install:
npm install --save-dev puppeteer jest-puppeteer

Configuration:
// jest-puppeteer.config.js
module.exports = {
  launch: {
    headless: true,
    slowMo: 50,
  },
  server: {
    command: 'npm start',
    port: 3000,
    launchTimeout: 10000,
  },
};

Create tests:
tests/user-flow.test.js

Test Scenario Design

Key User Flows

Identify and test critical flows:

Identify key user flows:

E-commerce app:
1. User registration/login
2. Browse products
3. Add to cart
4. Checkout and payment
5. View orders

Write E2E tests for each flow

User Journey Testing

Test complete user journeys:

typescript
describe('Complete Shopping Flow', () => {
  it('should complete the full flow from browsing to purchase', () => {
    // 1. Visit homepage
    cy.visit('/');
    cy.get('[data-testid="hero"]').should('be.visible');

    // 2. Search for products
    cy.get('[data-testid="search"]').type('laptop');
    cy.get('[data-testid="search-button"]').click();

    // 3. Select a product
    cy.get('[data-testid="product-card"]').first().click();
    cy.get('[data-testid="product-title"]').should('be.visible');

    // 4. Add to cart
    cy.get('[data-testid="add-to-cart"]').click();
    cy.get('[data-testid="cart-badge"]').should('contain', '1');

    // 5. View cart
    cy.get('[data-testid="cart-icon"]').click();
    cy.get('[data-testid="cart-item"]').should('have.length', 1);

    // 6. Checkout
    cy.get('[data-testid="checkout"]').click();

    // 7. Fill shipping info
    cy.get('[data-testid="address"]').type('Test Address');
    cy.get('[data-testid="phone"]').type('1234567890');

    // 8. Select payment method
    cy.get('[data-testid="payment-method"]').select('credit-card');

    // 9. Place order
    cy.get('[data-testid="place-order"]').click();

    // 10. Verify success
    cy.get('[data-testid="order-success"]').should('be.visible');
    cy.get('[data-testid="order-number"]').should('exist');
  });
});

Edge Case Testing

Test edge cases and error handling:

typescript
describe('Edge Cases', () => {
  it('should handle network errors', () => {
    // Simulate network error
    cy.intercept('POST', '/api/orders', {
      statusCode: 500,
      body: { error: 'Server error' },
    });

    cy.visit('/checkout');
    cy.get('[data-testid="place-order"]').click();

    cy.get('[data-testid="error-message"]')
      .should('be.visible')
      .and('contain', 'Order submission failed');
  });

  it('should handle session expiration', () => {
    // Clear authentication
    cy.clearCookies();

    cy.visit('/dashboard');

    // Should redirect to login page
    cy.url().should('include', '/login');
  });

  it('should handle concurrent operations', () => {
    // Test inventory competition
    cy.visit('/product/123');

    // Simulate out of stock
    cy.intercept('POST', '/api/cart', {
      statusCode: 409,
      body: { error: 'Out of stock' },
    });

    cy.get('[data-testid="add-to-cart"]').click();

    cy.get('[data-testid="error-message"]')
      .should('contain', 'Product is sold out');
  });
});

Test Data Management

Test Data Preparation

Prepare test data:

typescript
// cypress/support/commands.js
Cypress.Commands.add('seedDatabase', () => {
  cy.request('POST', '/api/test/seed', {
    users: [
      { username: 'testuser', email: 'test@example.com' },
    ],
    products: [
      { name: 'Test Product', price: 99.99 },
    ],
  });
});

Cypress.Commands.add('cleanDatabase', () => {
  cy.request('POST', '/api/test/clean');
});

// Usage
describe('Test Suite', () => {
  beforeEach(() => {
    cy.cleanDatabase();
    cy.seedDatabase();
  });

  it('test case', () => {
    // test code
  });
});

Test Isolation

Ensure isolation between tests:

typescript
describe('Test Isolation', () => {
  beforeEach(() => {
    // Reset state before each test
    cy.clearCookies();
    cy.clearLocalStorage();
    cy.cleanDatabase();
    cy.seedDatabase();
  });

  afterEach(() => {
    // Clean up after each test
    cy.cleanDatabase();
  });

  it('test 1', () => {
    // Independent test
  });

  it('test 2', () => {
    // Does not depend on test 1
  });
});

Test Helpers

Custom Commands

Create reusable test commands:

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

Cypress.Commands.add('addToCart', (productId) => {
  cy.visit(`/product/${productId}`);
  cy.get('[data-testid="add-to-cart"]').click();
  cy.get('[data-testid="cart-badge"]').should('exist');
});

// Usage
describe('Shopping Flow', () => {
  it('should allow logged-in user to shop', () => {
    cy.login('testuser', 'password');
    cy.addToCart('product-123');
    // Continue testing
  });
});

Page Object Pattern

Use the Page Object pattern to organize tests:

typescript
// cypress/pages/LoginPage.js
export class LoginPage {
  visit() {
    cy.visit('/login');
  }

  fillUsername(username) {
    cy.get('[data-testid="username"]').type(username);
    return this;
  }

  fillPassword(password) {
    cy.get('[data-testid="password"]').type(password);
    return this;
  }

  submit() {
    cy.get('[data-testid="submit"]').click();
    return this;
  }

  getErrorMessage() {
    return cy.get('[data-testid="error-message"]');
  }
}

// Usage
import { LoginPage } from '../pages/LoginPage';

describe('Login', () => {
  const loginPage = new LoginPage();

  it('should login successfully', () => {
    loginPage
      .visit()
      .fillUsername('testuser')
      .fillPassword('password')
      .submit();

    cy.url().should('include', '/dashboard');
  });

  it('should display error message', () => {
    loginPage
      .visit()
      .fillUsername('invalid')
      .fillPassword('wrong')
      .submit();

    loginPage
      .getErrorMessage()
      .should('contain', 'Invalid username or password');
  });
});

API Mocking and Interception

Mock API Responses

Mock API responses for testing:

typescript
describe('API Mocking', () => {
  it('should display product list', () => {
    // Mock API response
    cy.intercept('GET', '/api/products', {
      statusCode: 200,
      body: [
        { id: 1, name: 'Product 1', price: 99.99 },
        { id: 2, name: 'Product 2', price: 199.99 },
      ],
    });

    cy.visit('/products');

    cy.get('[data-testid="product-card"]').should('have.length', 2);
  });

  it('should handle loading state', () => {
    // Delay response
    cy.intercept('GET', '/api/products', (req) => {
      req.reply({
        delay: 2000,
        statusCode: 200,
        body: [],
      });
    });

    cy.visit('/products');

    // Should show loading state
    cy.get('[data-testid="loading"]').should('be.visible');

    // Wait for loading to complete
    cy.get('[data-testid="loading"]').should('not.exist');
  });
});

Verify API Calls

Verify API call correctness:

typescript
describe('API Verification', () => {
  it('should send correct request', () => {
    cy.intercept('POST', '/api/orders').as('createOrder');

    cy.visit('/checkout');
    cy.get('[data-testid="place-order"]').click();

    cy.wait('@createOrder').then((interception) => {
      expect(interception.request.body).to.deep.equal({
        items: [{ productId: 1, quantity: 1 }],
        address: 'Test Address',
        paymentMethod: 'credit-card',
      });
    });
  });
});

Visual Regression Testing

Screenshot Comparison

Perform visual regression testing:

typescript
describe('Visual Regression', () => {
  it('should match homepage snapshot', () => {
    cy.visit('/');
    cy.matchImageSnapshot('homepage');
  });

  it('should match product detail page snapshot', () => {
    cy.visit('/product/123');
    cy.matchImageSnapshot('product-detail');
  });
});

Responsive Testing

Test different screen sizes:

typescript
describe('Responsive Design', () => {
  const viewports = [
    { name: 'mobile', width: 375, height: 667 },
    { name: 'tablet', width: 768, height: 1024 },
    { name: 'desktop', width: 1920, height: 1080 },
  ];

  viewports.forEach(({ name, width, height }) => {
    it(`should display correctly on ${name}`, () => {
      cy.viewport(width, height);
      cy.visit('/');
      cy.matchImageSnapshot(`homepage-${name}`);
    });
  });
});

Performance Testing

Load Time Testing

Test page load performance:

typescript
describe('Performance', () => {
  it('should load homepage within 2 seconds', () => {
    cy.visit('/', {
      onBeforeLoad: (win) => {
        win.performance.mark('start');
      },
    });

    cy.window().then((win) => {
      win.performance.mark('end');
      win.performance.measure('pageLoad', 'start', 'end');
      const measure = win.performance.getEntriesByName('pageLoad')[0];
      expect(measure.duration).to.be.lessThan(2000);
    });
  });
});

CI/CD Integration

GitHub Actions Configuration

Run E2E tests in CI/CD:

yaml
# .github/workflows/e2e.yml
name: E2E Tests

on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build

      - name: Start server
        run: npm start &
        env:
          NODE_ENV: test

      - name: Wait for server
        run: npx wait-on http://localhost:3000

      - name: Run E2E tests
        run: npm run test:e2e

      - name: Upload screenshots
        if: failure()
        uses: actions/upload-artifact@v2
        with:
          name: cypress-screenshots
          path: cypress/screenshots

      - name: Upload videos
        if: failure()
        uses: actions/upload-artifact@v2
        with:
          name: cypress-videos
          path: cypress/videos

Test Reports

Generate Test Reports

Generate detailed test reports:

javascript
// cypress.config.js
import { defineConfig } from 'cypress';

export default defineConfig({
  reporter: 'mochawesome',
  reporterOptions: {
    reportDir: 'cypress/reports',
    overwrite: false,
    html: true,
    json: true,
  },
});

Analyze Test Results

Analyze and improve tests:

Analyze E2E test results:

Metrics:
- Test pass rate
- Test execution time
- Failed tests
- Flaky tests

Improvements:
- Fix failed tests
- Optimize slow tests
- Stabilize flaky tests
- Increase test coverage

Best Practices

  1. Test user scenarios, not implementation details
  2. Keep tests independent and isolated
  3. Use meaningful test data
  4. Avoid test flakiness
  5. Use waits and retries appropriately
  6. Use data-testid instead of CSS selectors
  7. Regularly review and maintain tests
  8. Run tests in CI/CD

Summary

An effective E2E testing workflow requires:

  • A clear testing strategy
  • An appropriate test framework
  • Good test organization
  • Reliable test data
  • Comprehensive CI/CD integration

With Claude Code's help, you can quickly write high-quality E2E tests and ensure overall application quality.

Released under the MIT License.