| name | backend/testing-guide |
| description | 后端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践 |
| type | methodology |
| agent | boss-backend |
后端测试编写指南
测试要求(强制)
职责边界:Backend Agent 是测试的编写者,QA Agent 是测试的验证者。
测试金字塔
| 测试类型 | 占比 | 要求 |
|---|
| 单元测试 | ~70% | Service 层、业务逻辑必须有测试 |
| 集成测试 | ~20% | API 端点、数据库操作测试 |
| E2E 测试 | ~10% | 必须编写,完整 API 流程测试 |
单元测试编写
Service 层测试
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');
});
});
});
业务逻辑测试
describe('OrderService', () => {
describe('calculateTotal', () => {
it('calculates total with discount', () => {
const items = [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 },
];
const discount = 0.1;
const total = orderService.calculateTotal(items, discount);
expect(total).toBe(225);
});
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 = { : , : };
productRepository..({ : , : });
result = orderService.(order);
(result.).();
});
(, () => {
order = { : , : };
productRepository..({ : , : });
result = orderService.(order);
(result.).();
(result.).();
});
});
});
集成测试编写
API 端点测试
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).(userData);
(response...).();
});
(, () => {
userData = {
: ,
: ,
};
response = (app)
.()
.(userData)
.();
(response..).();
(response...).();
});
(, () => {
userData = {
: ,
: ,
};
(app).().(userData);
response = (app)
.()
.(userData)
.();
(response...).();
});
});
(, {
(, () => {
createResponse = (app)
.()
.({ : , : });
userId = createResponse...;
response = (app)
.()
.();
(response...).(userId);
(response...).();
});
(, () => {
response = (app)
.()
.();
(response...).();
});
});
(, {
(, () => {
createResponse = (app)
.()
.({ : , : });
userId = createResponse...;
response = (app)
.()
.({ : })
.();
(response...).();
(response...).();
});
});
(, {
(, () => {
createResponse = (app)
.()
.({ : , : });
userId = createResponse...;
(app)
.()
.();
(app)
.()
.();
});
});
});
数据库操作测试
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.({ : { : user. } });
(found).();
});
});
(, {
(, () => {
userRepository.({ : , : });
user = userRepository.();
(user).();
(user?.).();
});
(, () => {
user = userRepository.();
(user).();
});
});
});
E2E 测试编写(必须)
API E2E 测试必须覆盖
- ✅ 创建资源(POST)
- ✅ 读取资源(GET)
- ✅ 更新资源(PUT/PATCH)
- ✅ 删除资源(DELETE)
- ✅ 完整业务流程(如:注册→登录→操作)
完整 CRUD 流程测试
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 () => {
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;
const getResponse = (app)
.()
.();
(getResponse...).();
(getResponse...).();
updateResponse = (app)
.()
.({ : })
.();
(updateResponse...).();
verifyResponse = (app)
.()
.();
(verifyResponse...).();
(app)
.()
.();
(app)
.()
.();
});
});
认证流程 E2E 测试
describe('Authentication Flow E2E', () => {
it('completes registration and login flow', async () => {
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;
const profileResponse = await request(app)
.get('/api/auth/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(profileResponse.body.data.email).toBe('alice@example.com');
(app)
.()
.(, )
.();
loginResponse = (app)
.()
.({
: ,
: ,
})
.();
(loginResponse...).();
});
(, () => {
(app)
.()
.({
: ,
: ,
})
.();
});
});
业务流程 E2E 测试
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 () => {
orderResponse = (app)
.()
.(, )
.({
: [{ productId, : }],
})
.();
orderId = orderResponse...;
(orderResponse...).();
(orderResponse...).();
paymentResponse = (app)
.()
.(, )
.({ : })
.();
(paymentResponse...).();
productResponse = (app)
.()
.();
(productResponse...).();
ordersResponse = (app)
.()
.(, )
.();
(ordersResponse...).();
(ordersResponse...[].).(orderId);
});
});
测试最佳实践
测试数据库隔离
使用测试数据库或事务回滚:
beforeAll(async () => {
process.env.DATABASE_URL = 'postgresql://localhost:5432/test_db';
await db.$connect();
});
beforeEach(async () => {
await db.$transaction(async (tx) => {
});
});
Mock 外部服务
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);
});
});
测试覆盖率要求
- 语句覆盖率:≥ 80%
- 分支覆盖率:≥ 75%
- 函数覆盖率:≥ 80%
- 行覆盖率:≥ 80%
运行覆盖率报告:
npm test -- --coverage
测试报告格式
实现完成后,在输出中包含:
测试添加:
| 类型 | 文件 | 描述 |
|---|
| 单元测试 | src/services/userService.test.ts | UserService 业务逻辑测试 |
| 集成测试 | src/controllers/userController.test.ts | User API 端点集成测试 |
| E2E 测试 | e2e/user-crud.test.ts | 用户 CRUD 完整流程 E2E 测试 |
测试结果:
- 通过:42 / 失败:0
- 覆盖率:87%
- E2E 测试:✅ 已编写并通过