skill/skills/backend/testing-guide/SKILL.md
后端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
npx skillsauth add echovic/boss-skill backend/testing-guideInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
职责边界:Backend Agent 是测试的编写者,QA Agent 是测试的验证者。
| 测试类型 | 占比 | 要求 | |----------|------|------| | 单元测试 | ~70% | Service 层、业务逻辑必须有测试 | | 集成测试 | ~20% | API 端点、数据库操作测试 | | E2E 测试 | ~10% | 必须编写,完整 API 流程测试 |
// services/userService.test.ts
import { UserService } from './userService';
import { UserRepository } from '../repositories/userRepository';
jest.mock('../repositories/userRepository');
describe('UserService', () => {
let userService: UserService;
let userRepository: jest.Mocked<UserRepository>;
beforeEach(() => {
userRepository = new UserRepository() as jest.Mocked<UserRepository>;
userService = new UserService(userRepository);
});
describe('getById', () => {
it('returns user when found', async () => {
const mockUser = { id: '1', name: 'Alice', email: '[email protected]' };
userRepository.findById.mockResolvedValue(mockUser);
const result = await userService.getById('1');
expect(result).toEqual(mockUser);
expect(userRepository.findById).toHaveBeenCalledWith('1');
});
it('throws NotFoundError when user not found', async () => {
userRepository.findById.mockResolvedValue(null);
await expect(userService.getById('999')).rejects.toThrow('User not found');
});
});
describe('create', () => {
it('creates user with valid data', async () => {
const createData = { name: 'Bob', email: '[email protected]' };
const mockUser = { id: '2', ...createData };
userRepository.findByEmail.mockResolvedValue(null);
userRepository.create.mockResolvedValue(mockUser);
const result = await userService.create(createData);
expect(result).toEqual(mockUser);
expect(userRepository.findByEmail).toHaveBeenCalledWith('[email protected]');
expect(userRepository.create).toHaveBeenCalledWith(createData);
});
it('throws ConflictError when email already exists', async () => {
const createData = { name: 'Bob', email: '[email protected]' };
userRepository.findByEmail.mockResolvedValue({ id: '1', name: 'Existing', email: '[email protected]' });
await expect(userService.create(createData)).rejects.toThrow('Email already exists');
});
});
});
// services/orderService.test.ts
describe('OrderService', () => {
describe('calculateTotal', () => {
it('calculates total with discount', () => {
const items = [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 },
];
const discount = 0.1; // 10% off
const total = orderService.calculateTotal(items, discount);
expect(total).toBe(225); // (200 + 50) * 0.9
});
it('handles zero discount', () => {
const items = [{ price: 100, quantity: 1 }];
const total = orderService.calculateTotal(items, 0);
expect(total).toBe(100);
});
});
describe('validateOrder', () => {
it('validates order with sufficient stock', async () => {
const order = { productId: '1', quantity: 5 };
productRepository.findById.mockResolvedValue({ id: '1', stock: 10 });
const result = await orderService.validateOrder(order);
expect(result.valid).toBe(true);
});
it('rejects order with insufficient stock', async () => {
const order = { productId: '1', quantity: 15 };
productRepository.findById.mockResolvedValue({ id: '1', stock: 10 });
const result = await orderService.validateOrder(order);
expect(result.valid).toBe(false);
expect(result.error).toBe('Insufficient stock');
});
});
});
// controllers/userController.test.ts
import request from 'supertest';
import { app } from '../app';
import { db } from '../db';
describe('User API', () => {
beforeEach(async () => {
// 清理测试数据库
await db.user.deleteMany();
});
afterAll(async () => {
await db.$disconnect();
});
describe('POST /api/users', () => {
it('creates a new user', async () => {
const userData = {
name: 'Alice',
email: '[email protected]',
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data).toMatchObject(userData);
expect(response.body.data.id).toBeDefined();
});
it('returns 400 for invalid email', async () => {
const userData = {
name: 'Bob',
email: 'invalid-email',
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
it('returns 409 for duplicate email', async () => {
const userData = {
name: 'Charlie',
email: '[email protected]',
};
// 创建第一个用户
await request(app).post('/api/users').send(userData);
// 尝试创建重复邮箱的用户
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(409);
expect(response.body.error.code).toBe('CONFLICT');
});
});
describe('GET /api/users/:id', () => {
it('returns user by id', async () => {
// 创建测试用户
const createResponse = await request(app)
.post('/api/users')
.send({ name: 'Dave', email: '[email protected]' });
const userId = createResponse.body.data.id;
// 获取用户
const response = await request(app)
.get(`/api/users/${userId}`)
.expect(200);
expect(response.body.data.id).toBe(userId);
expect(response.body.data.name).toBe('Dave');
});
it('returns 404 for non-existent user', async () => {
const response = await request(app)
.get('/api/users/non-existent-id')
.expect(404);
expect(response.body.error.code).toBe('NOT_FOUND');
});
});
describe('PUT /api/users/:id', () => {
it('updates user', async () => {
// 创建用户
const createResponse = await request(app)
.post('/api/users')
.send({ name: 'Eve', email: '[email protected]' });
const userId = createResponse.body.data.id;
// 更新用户
const response = await request(app)
.put(`/api/users/${userId}`)
.send({ name: 'Eve Updated' })
.expect(200);
expect(response.body.data.name).toBe('Eve Updated');
expect(response.body.data.email).toBe('[email protected]');
});
});
describe('DELETE /api/users/:id', () => {
it('deletes user', async () => {
// 创建用户
const createResponse = await request(app)
.post('/api/users')
.send({ name: 'Frank', email: '[email protected]' });
const userId = createResponse.body.data.id;
// 删除用户
await request(app)
.delete(`/api/users/${userId}`)
.expect(200);
// 验证用户已删除
await request(app)
.get(`/api/users/${userId}`)
.expect(404);
});
});
});
// repositories/userRepository.test.ts
import { UserRepository } from './userRepository';
import { db } from '../db';
describe('UserRepository', () => {
let userRepository: UserRepository;
beforeEach(async () => {
userRepository = new UserRepository();
await db.user.deleteMany();
});
afterAll(async () => {
await db.$disconnect();
});
describe('create', () => {
it('creates user in database', async () => {
const userData = { name: 'Alice', email: '[email protected]' };
const user = await userRepository.create(userData);
expect(user.id).toBeDefined();
expect(user.name).toBe('Alice');
// 验证数据库中存在
const found = await db.user.findUnique({ where: { id: user.id } });
expect(found).toBeTruthy();
});
});
describe('findByEmail', () => {
it('finds user by email', async () => {
await userRepository.create({ name: 'Bob', email: '[email protected]' });
const user = await userRepository.findByEmail('[email protected]');
expect(user).toBeTruthy();
expect(user?.name).toBe('Bob');
});
it('returns null for non-existent email', async () => {
const user = await userRepository.findByEmail('[email protected]');
expect(user).toBeNull();
});
});
});
// e2e/user-crud.test.ts
import request from 'supertest';
import { app } from '../app';
import { db } from '../db';
describe('User CRUD E2E', () => {
beforeAll(async () => {
await db.user.deleteMany();
});
afterAll(async () => {
await db.$disconnect();
});
it('completes full user lifecycle', async () => {
// 1. 创建用户
const createResponse = await request(app)
.post('/api/users')
.send({
name: 'John Doe',
email: '[email protected]',
password: 'password123',
})
.expect(201);
expect(createResponse.body.success).toBe(true);
const userId = createResponse.body.data.id;
// 2. 读取用户
const getResponse = await request(app)
.get(`/api/users/${userId}`)
.expect(200);
expect(getResponse.body.data.name).toBe('John Doe');
expect(getResponse.body.data.email).toBe('[email protected]');
// 3. 更新用户
const updateResponse = await request(app)
.put(`/api/users/${userId}`)
.send({ name: 'John Updated' })
.expect(200);
expect(updateResponse.body.data.name).toBe('John Updated');
// 4. 验证更新
const verifyResponse = await request(app)
.get(`/api/users/${userId}`)
.expect(200);
expect(verifyResponse.body.data.name).toBe('John Updated');
// 5. 删除用户
await request(app)
.delete(`/api/users/${userId}`)
.expect(200);
// 6. 验证删除
await request(app)
.get(`/api/users/${userId}`)
.expect(404);
});
});
// e2e/auth-flow.test.ts
describe('Authentication Flow E2E', () => {
it('completes registration and login flow', async () => {
// 1. 注册
const registerResponse = await request(app)
.post('/api/auth/register')
.send({
name: 'Alice',
email: '[email protected]',
password: 'securePassword123',
})
.expect(201);
expect(registerResponse.body.data.token).toBeDefined();
const token = registerResponse.body.data.token;
// 2. 使用 Token 访问受保护资源
const profileResponse = await request(app)
.get('/api/auth/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(profileResponse.body.data.email).toBe('[email protected]');
// 3. 登出
await request(app)
.post('/api/auth/logout')
.set('Authorization', `Bearer ${token}`)
.expect(200);
// 4. 登录
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: '[email protected]',
password: 'securePassword123',
})
.expect(200);
expect(loginResponse.body.data.token).toBeDefined();
});
it('rejects invalid credentials', async () => {
await request(app)
.post('/api/auth/login')
.send({
email: '[email protected]',
password: 'wrongPassword',
})
.expect(401);
});
});
// e2e/order-flow.test.ts
describe('Order Flow E2E', () => {
let authToken: string;
let productId: string;
beforeAll(async () => {
// 注册并登录
const registerResponse = await request(app)
.post('/api/auth/register')
.send({ name: 'Buyer', email: '[email protected]', password: 'pass123' });
authToken = registerResponse.body.data.token;
// 创建测试商品
const productResponse = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: 'Test Product', price: 100, stock: 10 });
productId = productResponse.body.data.id;
});
it('completes order creation and payment flow', async () => {
// 1. 创建订单
const orderResponse = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({
items: [{ productId, quantity: 2 }],
})
.expect(201);
const orderId = orderResponse.body.data.id;
expect(orderResponse.body.data.status).toBe('pending');
expect(orderResponse.body.data.total).toBe(200);
// 2. 支付订单
const paymentResponse = await request(app)
.post(`/api/orders/${orderId}/pay`)
.set('Authorization', `Bearer ${authToken}`)
.send({ paymentMethod: 'credit_card' })
.expect(200);
expect(paymentResponse.body.data.status).toBe('paid');
// 3. 验证库存减少
const productResponse = await request(app)
.get(`/api/products/${productId}`)
.expect(200);
expect(productResponse.body.data.stock).toBe(8); // 10 - 2
// 4. 获取订单历史
const ordersResponse = await request(app)
.get('/api/orders')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(ordersResponse.body.data.items).toHaveLength(1);
expect(ordersResponse.body.data.items[0].id).toBe(orderId);
});
});
使用测试数据库或事务回滚:
// 方案 1:使用测试数据库
beforeAll(async () => {
process.env.DATABASE_URL = 'postgresql://localhost:5432/test_db';
await db.$connect();
});
// 方案 2:使用事务回滚
beforeEach(async () => {
await db.$transaction(async (tx) => {
// 测试在事务中运行
});
});
// 模拟第三方 API
jest.mock('../services/paymentGateway', () => ({
processPayment: jest.fn().mockResolvedValue({ success: true, transactionId: 'tx123' }),
}));
// 模拟邮件服务
jest.mock('../services/emailService', () => ({
sendEmail: jest.fn().mockResolvedValue(true),
}));
describe('Boundary conditions', () => {
it('handles empty list', async () => {
const response = await request(app).get('/api/users').expect(200);
expect(response.body.data.items).toEqual([]);
});
it('handles maximum page size', async () => {
const response = await request(app)
.get('/api/users?pageSize=1000')
.expect(400);
expect(response.body.error.message).toContain('pageSize');
});
it('handles invalid UUID', async () => {
await request(app)
.get('/api/users/invalid-uuid')
.expect(400);
});
});
it('handles concurrent requests', async () => {
const requests = Array(10).fill(null).map(() =>
request(app).post('/api/users').send({ name: 'User', email: `user${Math.random()}@example.com` })
);
const responses = await Promise.all(requests);
responses.forEach(response => {
expect(response.status).toBe(201);
});
});
运行覆盖率报告:
npm test -- --coverage
实现完成后,在输出中包含:
测试添加:
| 类型 | 文件 | 描述 |
|------|------|------|
| 单元测试 | src/services/userService.test.ts | UserService 业务逻辑测试 |
| 集成测试 | src/controllers/userController.test.ts | User API 端点集成测试 |
| E2E 测试 | e2e/user-crud.test.ts | 用户 CRUD 完整流程 E2E 测试 |
测试结果:
testing
交互规范,定义加载状态、空状态、反馈机制、动效、无障碍等交互细节
content-media
设计变体模式,产出2-3个设计方案及 tradeoff 分析,供用户选择后确定最终方案
content-media
设计系统规范,包含颜色、字体、间距、圆角、阴影、动效等基础设计token
testing
UI组件规范,定义按钮、输入框、卡片等基础组件的变体、尺寸、状态