E2E 测试工作流
端到端(E2E)测试是验证应用整体功能的关键手段。本指南介绍如何使用 Claude Code 建立高效的 E2E 测试工作流。
E2E 测试概述
什么是 E2E 测试
E2E 测试模拟真实用户操作,验证应用从前端到后端的完整流程。
E2E 测试的价值
- 验证用户场景
- 发现集成问题
- 确保功能正常
- 提高发布信心
- 减少生产问题
测试金字塔
/\
/E2E\ 少量 E2E 测试
/------\
/集成测试\ 适量集成测试
/----------\
/ 单元测试 \ 大量单元测试
/--------------\使用 Claude 编写 E2E 测试
生成测试用例
让 Claude 生成 E2E 测试:
为用户注册流程编写 E2E 测试:
流程:
1. 访问注册页面
2. 填写表单(用户名、邮箱、密码)
3. 提交表单
4. 验证成功消息
5. 验证跳转到首页
使用 Cypress
包含:
- 正常流程
- 表单验证
- 错误处理测试用例示例
typescript
describe('用户注册', () => {
beforeEach(() => {
cy.visit('/register');
});
it('应该成功注册新用户', () => {
// 填写表单
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!');
// 提交表单
cy.get('[data-testid="submit"]').click();
// 验证结果
cy.get('[data-testid="success-message"]')
.should('be.visible')
.and('contain', '注册成功');
// 验证跳转
cy.url().should('include', '/dashboard');
});
it('应该验证必填字段', () => {
cy.get('[data-testid="submit"]').click();
cy.get('[data-testid="username-error"]')
.should('be.visible')
.and('contain', '用户名不能为空');
});
it('应该验证邮箱格式', () => {
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', '邮箱格式不正确');
});
it('应该验证密码强度', () => {
cy.get('[data-testid="password"]').type('weak');
cy.get('[data-testid="submit"]').click();
cy.get('[data-testid="password-error"]')
.should('be.visible')
.and('contain', '密码强度不足');
});
it('应该处理重复邮箱', () => {
// 使用已存在的邮箱
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', '邮箱已被注册');
});
});测试框架选择
Cypress
使用 Cypress 进行 E2E 测试:
设置 Cypress 测试环境:
安装:
npm install --save-dev cypress
配置:
// cypress.config.js
export default {
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
video: false,
screenshotOnRunFailure: true,
},
};
创建测试:
cypress/e2e/user-flow.cy.jsPlaywright
使用 Playwright 进行 E2E 测试:
设置 Playwright 测试环境:
安装:
npm install --save-dev @playwright/test
配置:
// 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'] } },
],
};
创建测试:
tests/user-flow.spec.jsPuppeteer
使用 Puppeteer 进行 E2E 测试:
设置 Puppeteer 测试环境:
安装:
npm install --save-dev puppeteer jest-puppeteer
配置:
// jest-puppeteer.config.js
module.exports = {
launch: {
headless: true,
slowMo: 50,
},
server: {
command: 'npm start',
port: 3000,
launchTimeout: 10000,
},
};
创建测试:
tests/user-flow.test.js测试场景设计
关键用户流程
识别和测试关键流程:
识别关键用户流程:
电商应用:
1. 用户注册/登录
2. 浏览商品
3. 添加到购物车
4. 结账支付
5. 查看订单
为每个流程编写 E2E 测试用户旅程测试
测试完整的用户旅程:
typescript
describe('完整购物流程', () => {
it('应该完成从浏览到购买的完整流程', () => {
// 1. 访问首页
cy.visit('/');
cy.get('[data-testid="hero"]').should('be.visible');
// 2. 搜索商品
cy.get('[data-testid="search"]').type('笔记本电脑');
cy.get('[data-testid="search-button"]').click();
// 3. 选择商品
cy.get('[data-testid="product-card"]').first().click();
cy.get('[data-testid="product-title"]').should('be.visible');
// 4. 添加到购物车
cy.get('[data-testid="add-to-cart"]').click();
cy.get('[data-testid="cart-badge"]').should('contain', '1');
// 5. 查看购物车
cy.get('[data-testid="cart-icon"]').click();
cy.get('[data-testid="cart-item"]').should('have.length', 1);
// 6. 结账
cy.get('[data-testid="checkout"]').click();
// 7. 填写配送信息
cy.get('[data-testid="address"]').type('测试地址');
cy.get('[data-testid="phone"]').type('13800138000');
// 8. 选择支付方式
cy.get('[data-testid="payment-method"]').select('credit-card');
// 9. 确认订单
cy.get('[data-testid="place-order"]').click();
// 10. 验证成功
cy.get('[data-testid="order-success"]').should('be.visible');
cy.get('[data-testid="order-number"]').should('exist');
});
});边界情况测试
测试边界情况和错误处理:
typescript
describe('边界情况', () => {
it('应该处理网络错误', () => {
// 模拟网络错误
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', '订单提交失败');
});
it('应该处理会话过期', () => {
// 清除认证
cy.clearCookies();
cy.visit('/dashboard');
// 应该重定向到登录页
cy.url().should('include', '/login');
});
it('应该处理并发操作', () => {
// 测试库存竞争
cy.visit('/product/123');
// 模拟库存不足
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', '商品已售罄');
});
});测试数据管理
测试数据准备
准备测试数据:
typescript
// cypress/support/commands.js
Cypress.Commands.add('seedDatabase', () => {
cy.request('POST', '/api/test/seed', {
users: [
{ username: 'testuser', email: 'test@example.com' },
],
products: [
{ name: '测试商品', price: 99.99 },
],
});
});
Cypress.Commands.add('cleanDatabase', () => {
cy.request('POST', '/api/test/clean');
});
// 使用
describe('测试套件', () => {
beforeEach(() => {
cy.cleanDatabase();
cy.seedDatabase();
});
it('测试用例', () => {
// 测试代码
});
});测试隔离
确保测试之间的隔离:
typescript
describe('测试隔离', () => {
beforeEach(() => {
// 每个测试前重置状态
cy.clearCookies();
cy.clearLocalStorage();
cy.cleanDatabase();
cy.seedDatabase();
});
afterEach(() => {
// 每个测试后清理
cy.cleanDatabase();
});
it('测试 1', () => {
// 独立的测试
});
it('测试 2', () => {
// 不依赖测试 1
});
});测试辅助功能
自定义命令
创建可复用的测试命令:
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');
});
// 使用
describe('购物流程', () => {
it('应该允许已登录用户购物', () => {
cy.login('testuser', 'password');
cy.addToCart('product-123');
// 继续测试
});
});Page Object 模式
使用 Page Object 模式组织测试:
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"]');
}
}
// 使用
import { LoginPage } from '../pages/LoginPage';
describe('登录', () => {
const loginPage = new LoginPage();
it('应该成功登录', () => {
loginPage
.visit()
.fillUsername('testuser')
.fillPassword('password')
.submit();
cy.url().should('include', '/dashboard');
});
it('应该显示错误消息', () => {
loginPage
.visit()
.fillUsername('invalid')
.fillPassword('wrong')
.submit();
loginPage
.getErrorMessage()
.should('contain', '用户名或密码错误');
});
});API 模拟和拦截
模拟 API 响应
模拟 API 响应进行测试:
typescript
describe('API 模拟', () => {
it('应该显示商品列表', () => {
// 模拟 API 响应
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: [
{ id: 1, name: '商品 1', price: 99.99 },
{ id: 2, name: '商品 2', price: 199.99 },
],
});
cy.visit('/products');
cy.get('[data-testid="product-card"]').should('have.length', 2);
});
it('应该处理加载状态', () => {
// 延迟响应
cy.intercept('GET', '/api/products', (req) => {
req.reply({
delay: 2000,
statusCode: 200,
body: [],
});
});
cy.visit('/products');
// 应该显示加载状态
cy.get('[data-testid="loading"]').should('be.visible');
// 等待加载完成
cy.get('[data-testid="loading"]').should('not.exist');
});
});验证 API 调用
验证 API 调用的正确性:
typescript
describe('API 验证', () => {
it('应该发送正确的请求', () => {
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: '测试地址',
paymentMethod: 'credit-card',
});
});
});
});视觉回归测试
截图对比
进行视觉回归测试:
typescript
describe('视觉回归', () => {
it('应该匹配首页快照', () => {
cy.visit('/');
cy.matchImageSnapshot('homepage');
});
it('应该匹配商品详情页快照', () => {
cy.visit('/product/123');
cy.matchImageSnapshot('product-detail');
});
});响应式测试
测试不同屏幕尺寸:
typescript
describe('响应式设计', () => {
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(`应该在 ${name} 上正确显示`, () => {
cy.viewport(width, height);
cy.visit('/');
cy.matchImageSnapshot(`homepage-${name}`);
});
});
});性能测试
加载时间测试
测试页面加载性能:
typescript
describe('性能', () => {
it('应该在 2 秒内加载首页', () => {
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 集成
GitHub Actions 配置
在 CI/CD 中运行 E2E 测试:
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测试报告
生成测试报告
生成详细的测试报告:
javascript
// cypress.config.js
import { defineConfig } from 'cypress';
export default defineConfig({
reporter: 'mochawesome',
reporterOptions: {
reportDir: 'cypress/reports',
overwrite: false,
html: true,
json: true,
},
});分析测试结果
分析和改进测试:
分析 E2E 测试结果:
指标:
- 测试通过率
- 测试执行时间
- 失败的测试
- 不稳定的测试
改进:
- 修复失败的测试
- 优化慢的测试
- 稳定不稳定的测试
- 增加测试覆盖最佳实践
- 测试用户场景而非实现细节
- 保持测试独立和隔离
- 使用有意义的测试数据
- 避免测试不稳定
- 合理使用等待和重试
- 使用 data-testid 而非 CSS 选择器
- 定期审查和维护测试
- 在 CI/CD 中运行测试
总结
有效的 E2E 测试工作流需要:
- 清晰的测试策略
- 合适的测试框架
- 良好的测试组织
- 可靠的测试数据
- 完善的 CI/CD 集成
通过 Claude Code 的帮助,你可以快速编写高质量的 E2E 测试,确保应用的整体质量。