Claude Code 测试策略与实践
构建完整的测试体系,从单元测试到端到端测试,确保代码质量和系统稳定性。
测试金字塔
🔬 单元测试 (70%)
快速、独立、大量
🔗 集成测试 (20%)
组件间交互测试
🎭 端到端测试 (10%)
完整用户流程测试
单元测试最佳实践
测试驱动开发 (TDD)
// 1. 先写测试
describe('UserService', () => {
describe('createUser', () => {
it('should create user with valid data', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'securePassword123'
};
const user = await userService.createUser(userData);
expect(user.id).toBeDefined();
expect(user.name).toBe(userData.name);
expect(user.email).toBe(userData.email);
expect(user.password).not.toBe(userData.password); // 应该被加密
});
it('should throw error for invalid email', async () => {
const userData = {
name: 'John Doe',
email: 'invalid-email',
password: 'securePassword123'
};
await expect(userService.createUser(userData))
.rejects.toThrow('Invalid email format');
});
});
});
// 2. 实现功能
class UserService {
async createUser(userData) {
// 验证邮箱格式
if (!this.isValidEmail(userData.email)) {
throw new Error('Invalid email format');
}
// 加密密码
const hashedPassword = await bcrypt.hash(userData.password, 10);
// 创建用户
const user = await this.userRepository.create({
...userData,
password: hashedPassword,
id: generateId(),
createdAt: new Date()
});
return user;
}
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
Mock和Stub的使用
// 依赖注入和Mock
describe('OrderService', () => {
let orderService;
let mockUserService;
let mockPaymentService;
let mockInventoryService;
beforeEach(() => {
mockUserService = {
findById: jest.fn(),
validateUser: jest.fn()
};
mockPaymentService = {
processPayment: jest.fn()
};
mockInventoryService = {
checkStock: jest.fn(),
reserveItems: jest.fn()
};
orderService = new OrderService({
userService: mockUserService,
paymentService: mockPaymentService,
inventoryService: mockInventoryService
});
});
it('should create order successfully', async () => {
// 设置Mock返回值
mockUserService.findById.mockResolvedValue({
id: 'user-1',
name: 'John Doe',
email: 'john@example.com'
});
mockInventoryService.checkStock.mockResolvedValue(true);
mockInventoryService.reserveItems.mockResolvedValue(['item-1']);
mockPaymentService.processPayment.mockResolvedValue({
transactionId: 'tx-123',
status: 'success'
});
const orderData = {
userId: 'user-1',
items: [{ id: 'item-1', quantity: 2 }],
totalAmount: 100
};
const order = await orderService.createOrder(orderData);
// 验证调用
expect(mockUserService.findById).toHaveBeenCalledWith('user-1');
expect(mockInventoryService.checkStock).toHaveBeenCalledWith(orderData.items);
expect(mockPaymentService.processPayment).toHaveBeenCalledWith({
amount: 100,
userId: 'user-1'
});
// 验证结果
expect(order.id).toBeDefined();
expect(order.status).toBe('confirmed');
});
});
集成测试策略
API集成测试
// API集成测试
describe('User API Integration', () => {
let app;
let testDb;
beforeAll(async () => {
// 设置测试数据库
testDb = await setupTestDatabase();
app = createApp({ database: testDb });
});
afterAll(async () => {
await cleanupTestDatabase(testDb);
});
beforeEach(async () => {
await testDb.clear();
});
describe('POST /api/users', () => {
it('should create user and return 201', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'securePassword123'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body.user.id).toBeDefined();
expect(response.body.user.name).toBe(userData.name);
expect(response.body.user.email).toBe(userData.email);
expect(response.body.user.password).toBeUndefined(); // 不应返回密码
// 验证数据库中的数据
const userInDb = await testDb.users.findById(response.body.user.id);
expect(userInDb).toBeDefined();
expect(userInDb.password).not.toBe(userData.password); // 应该被加密
});
it('should return 400 for duplicate email', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'securePassword123'
};
// 先创建一个用户
await request(app)
.post('/api/users')
.send(userData)
.expect(201);
// 尝试创建相同邮箱的用户
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(400);
expect(response.body.error).toContain('Email already exists');
});
});
});
数据库集成测试
// 数据库集成测试
describe('UserRepository Integration', () => {
let repository;
let testDb;
beforeAll(async () => {
testDb = await createTestDatabase();
repository = new UserRepository(testDb);
});
afterEach(async () => {
await testDb.users.deleteMany({});
});
it('should handle concurrent user creation', async () => {
const users = Array.from({ length: 10 }, (_, i) => ({
name: `User ${i}`,
email: `user${i}@example.com`,
password: 'password123'
}));
// 并发创建用户
const promises = users.map(user => repository.create(user));
const results = await Promise.all(promises);
expect(results).toHaveLength(10);
results.forEach((user, index) => {
expect(user.id).toBeDefined();
expect(user.email).toBe(users[index].email);
});
// 验证数据库中的数据
const allUsers = await repository.findAll();
expect(allUsers).toHaveLength(10);
});
it('should maintain referential integrity', async () => {
// 创建用户
const user = await repository.create({
name: 'John Doe',
email: 'john@example.com',
password: 'password123'
});
// 创建用户的订单
const order = await testDb.orders.create({
userId: user.id,
items: [{ name: 'Product 1', price: 100 }],
total: 100
});
// 尝试删除有订单的用户应该失败
await expect(repository.delete(user.id))
.rejects.toThrow('Cannot delete user with existing orders');
// 先删除订单,再删除用户应该成功
await testDb.orders.delete(order.id);
await expect(repository.delete(user.id)).resolves.toBeTruthy();
});
});
端到端测试
Playwright E2E测试
// E2E测试示例
import { test, expect } from '@playwright/test';
test.describe('User Registration Flow', () => {
test('should complete user registration successfully', async ({ page }) => {
// 访问注册页面
await page.goto('/register');
// 填写注册表单
await page.fill('[data-testid="name-input"]', 'John Doe');
await page.fill('[data-testid="email-input"]', 'john@example.com');
await page.fill('[data-testid="password-input"]', 'securePassword123');
await page.fill('[data-testid="confirm-password-input"]', 'securePassword123');
// 提交表单
await page.click('[data-testid="register-button"]');
// 验证成功消息
await expect(page.locator('[data-testid="success-message"]'))
.toContainText('Registration successful');
// 验证重定向到登录页面
await expect(page).toHaveURL('/login');
// 验证可以使用新账户登录
await page.fill('[data-testid="email-input"]', 'john@example.com');
await page.fill('[data-testid="password-input"]', 'securePassword123');
await page.click('[data-testid="login-button"]');
// 验证登录成功
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="user-name"]'))
.toContainText('John Doe');
});
test('should show validation errors for invalid input', async ({ page }) => {
await page.goto('/register');
// 提交空表单
await page.click('[data-testid="register-button"]');
// 验证错误消息
await expect(page.locator('[data-testid="name-error"]'))
.toContainText('Name is required');
await expect(page.locator('[data-testid="email-error"]'))
.toContainText('Email is required');
await expect(page.locator('[data-testid="password-error"]'))
.toContainText('Password is required');
// 填写无效邮箱
await page.fill('[data-testid="email-input"]', 'invalid-email');
await page.click('[data-testid="register-button"]');
await expect(page.locator('[data-testid="email-error"]'))
.toContainText('Please enter a valid email');
});
});
性能测试
// 性能测试
import { test, expect } from '@playwright/test';
test.describe('Performance Tests', () => {
test('should load dashboard within performance budget', async ({ page }) => {
// 开始性能监控
await page.goto('/dashboard');
// 等待页面完全加载
await page.waitForLoadState('networkidle');
// 获取性能指标
const performanceMetrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0];
return {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime,
firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime
};
});
// 验证性能指标
expect(performanceMetrics.domContentLoaded).toBeLessThan(2000); // 2秒内
expect(performanceMetrics.loadComplete).toBeLessThan(3000); // 3秒内
expect(performanceMetrics.firstContentfulPaint).toBeLessThan(1500); // 1.5秒内
});
test('should handle concurrent users', async ({ browser }) => {
const contexts = await Promise.all(
Array.from({ length: 10 }, () => browser.newContext())
);
const pages = await Promise.all(
contexts.map(context => context.newPage())
);
// 并发访问
const startTime = Date.now();
await Promise.all(
pages.map(page => page.goto('/dashboard'))
);
const endTime = Date.now();
// 验证所有页面都成功加载
for (const page of pages) {
await expect(page.locator('[data-testid="dashboard-title"]'))
.toBeVisible();
}
// 验证响应时间
const totalTime = endTime - startTime;
expect(totalTime).toBeLessThan(5000); // 5秒内完成所有请求
// 清理
await Promise.all(contexts.map(context => context.close()));
});
});
测试自动化与CI/CD
GitHub Actions配置
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
integration-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Start application
run: |
npm run build
npm run start &
npx wait-on http://localhost:3000
- name: Run E2E tests
run: npm run test:e2e
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
测试最佳实践总结
测试策略要点:
- ✅ 遵循测试金字塔原则,重点关注单元测试
- ✅ 使用TDD方法,先写测试再写代码
- ✅ 保持测试独立性,避免测试间相互依赖
- ✅ 使用有意义的测试名称和描述
- ✅ 定期重构测试代码,保持可维护性
- ✅ 集成CI/CD流程,自动化测试执行
目标指标:代码覆盖率 > 80%,测试执行时间 < 10分钟,E2E测试通过率> 95%