with one click
backend-testing-guide
后端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
后端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
| name | backend/testing-guide |
| description | 后端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践 |
| type | methodology |
| agent | boss-backend |
职责边界: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: 'alice@example.com' };
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: 'bob@example.com' };
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('bob@example.com');
expect(userRepository.create).toHaveBeenCalledWith(createData);
});
it('throws ConflictError when email already exists', async () => {
const createData = { name: 'Bob', email: 'existing@example.com' };
userRepository.findByEmail.mockResolvedValue({ id: '1', name: 'Existing', email: 'existing@example.com' });
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: 'alice@example.com',
};
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: 'duplicate@example.com',
};
// 创建第一个用户
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: 'dave@example.com' });
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: 'eve@example.com' });
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('eve@example.com');
});
});
describe('DELETE /api/users/:id', () => {
it('deletes user', async () => {
// 创建用户
const createResponse = await request(app)
.post('/api/users')
.send({ name: 'Frank', email: 'frank@example.com' });
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: 'alice@example.com' };
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: 'bob@example.com' });
const user = await userRepository.findByEmail('bob@example.com');
expect(user).toBeTruthy();
expect(user?.name).toBe('Bob');
});
it('returns null for non-existent email', async () => {
const user = await userRepository.findByEmail('nonexistent@example.com');
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: 'john@example.com',
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('john@example.com');
// 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: 'alice@example.com',
password: 'password123',
})
.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('alice@example.com');
// 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: 'alice@example.com',
password: 'password123',
})
.expect(200);
expect(loginResponse.body.data.token).toBeDefined();
});
it('rejects invalid credentials', async () => {
await request(app)
.post('/api/auth/login')
.send({
email: 'alice@example.com',
password: 'wrong-example',
})
.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: 'buyer@example.com', 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 测试 |
测试结果:
BMAD 全自动研发流水线编排器。编排 9 个专业 Agent(PM、架构师、UI Designer、Tech Lead、Scrum Master、Frontend、Backend、QA、DevOps)从需求到部署。 Triggers: 'boss mode', '/boss', '全自动开发', '从需求到部署', '帮我做一个', 'build this', 'ship it', '全流程', '自动化开发', '一键开发', 'start a project', 'new feature' Does NOT trigger: - 单文件修改或简单 bug 修复(直接编辑即可) - 纯代码阅读或解释(使用 read 工具) - 已有 pipeline 正在运行时的重复启动 - 极小事(预计 <30 分钟人工可完成、不需要 PRD/架构/门禁记录) Output: 完整项目代码 + PRD/架构/UI/测试/部署文档,写入 .boss/<feature>/ 目录
自动生成 CHANGELOG,基于 git 提交历史和 pipeline 产物信息,遵循 Conventional Commits 和 Keep a Changelog 规范
从CEO/战略视角进行商业价值评审,评估市场契合度、ROI、竞争优势、风险和战略对齐
设计变体模式,产出2-3个设计方案及 tradeoff 分析,供用户选择后确定最终方案
前端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
Playwright E2E 测试完整方法论,涵盖项目初始化、Page Object Model、认证复用、API Mock、视觉回归、多浏览器测试、CI 集成和调试技巧