- name
- backend/testing-guide
- description
- 后端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
- type
- methodology
- agent
- boss-backend
- metadata
- {"internal":true}
# 后端测试编写指南
## 测试要求(强制)
> **职责边界**:Backend Agent 是测试的**编写者**,QA Agent 是测试的**验证者**。
### 测试金字塔
| 测试类型 | 占比 | 要求 |
|----------|------|------|
| **单元测试** | ~70% | Service 层、业务逻辑必须有测试 |
| **集成测试** | ~20% | API 端点、数据库操作测试 |
| **E2E 测试** | ~10% | **必须编写**,完整 API 流程测试 |
## 单元测试编写
### Service 层测试
```typescript
// 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');
});
});
});
```
### 业务逻辑测试
```typescript
// 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');
});
});
});
```
## 集成测试编写
### API 端点测试
```typescript
// 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);
});
});
});
```
### 数据库操作测试
```typescript
// 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 测试编写(必须)
### API E2E 测试必须覆盖
- ✅ 创建资源(POST)
- ✅ 读取资源(GET)
- ✅ 更新资源(PUT/PATCH)
- ✅ 删除资源(DELETE)
- ✅ 完整业务流程(如:注册→登录→操作)
### 完整 CRUD 流程测试
```typescript
// 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',
Auf GitHub ansehen