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 handlingTest Case Example
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.jsPlaywright
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.jsPuppeteer
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.jsTest 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 flowUser Journey Testing
Test complete user journeys:
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:
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:
// 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:
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:
// 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:
// 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:
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:
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:
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:
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:
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:
# .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/videosTest Reports
Generate Test Reports
Generate detailed test reports:
// 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 coverageBest Practices
- Test user scenarios, not implementation details
- Keep tests independent and isolated
- Use meaningful test data
- Avoid test flakiness
- Use waits and retries appropriately
- Use data-testid instead of CSS selectors
- Regularly review and maintain tests
- 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.