$npx -y skills add echoVic/boss-skill --skill testing-guide后端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
| 1 | # 后端测试编写指南 |
| 2 | |
| 3 | ## 测试要求(强制) |
| 4 | |
| 5 | > **职责边界**:Backend Agent 是测试的**编写者**,QA Agent 是测试的**验证者**。 |
| 6 | |
| 7 | ### 测试金字塔 |
| 8 | |
| 9 | | 测试类型 | 占比 | 要求 | |
| 10 | |----------|------|------| |
| 11 | | **单元测试** | ~70% | Service 层、业务逻辑必须有测试 | |
| 12 | | **集成测试** | ~20% | API 端点、数据库操作测试 | |
| 13 | | **E2E 测试** | ~10% | **必须编写**,完整 API 流程测试 | |
| 14 | |
| 15 | ## 单元测试编写 |
| 16 | |
| 17 | ### Service 层测试 |
| 18 | |
| 19 | ```typescript |
| 20 | // services/userService.test.ts |
| 21 | import { UserService } from './userService'; |
| 22 | import { UserRepository } from '../repositories/userRepository'; |
| 23 | |
| 24 | jest.mock('../repositories/userRepository'); |
| 25 | |
| 26 | describe('UserService', () => { |
| 27 | let userService: UserService; |
| 28 | let userRepository: jest.Mocked<UserRepository>; |
| 29 | |
| 30 | beforeEach(() => { |
| 31 | userRepository = new UserRepository() as jest.Mocked<UserRepository>; |
| 32 | userService = new UserService(userRepository); |
| 33 | }); |
| 34 | |
| 35 | describe('getById', () => { |
| 36 | it('returns user when found', async () => { |
| 37 | const mockUser = { id: '1', name: 'Alice', email: 'alice@example.com' }; |
| 38 | userRepository.findById.mockResolvedValue(mockUser); |
| 39 | |
| 40 | const result = await userService.getById('1'); |
| 41 | |
| 42 | expect(result).toEqual(mockUser); |
| 43 | expect(userRepository.findById).toHaveBeenCalledWith('1'); |
| 44 | }); |
| 45 | |
| 46 | it('throws NotFoundError when user not found', async () => { |
| 47 | userRepository.findById.mockResolvedValue(null); |
| 48 | |
| 49 | await expect(userService.getById('999')).rejects.toThrow('User not found'); |
| 50 | }); |
| 51 | }); |
| 52 | |
| 53 | describe('create', () => { |
| 54 | it('creates user with valid data', async () => { |
| 55 | const createData = { name: 'Bob', email: 'bob@example.com' }; |
| 56 | const mockUser = { id: '2', ...createData }; |
| 57 | |
| 58 | userRepository.findByEmail.mockResolvedValue(null); |
| 59 | userRepository.create.mockResolvedValue(mockUser); |
| 60 | |
| 61 | const result = await userService.create(createData); |
| 62 | |
| 63 | expect(result).toEqual(mockUser); |
| 64 | expect(userRepository.findByEmail).toHaveBeenCalledWith('bob@example.com'); |
| 65 | expect(userRepository.create).toHaveBeenCalledWith(createData); |
| 66 | }); |
| 67 | |
| 68 | it('throws ConflictError when email already exists', async () => { |
| 69 | const createData = { name: 'Bob', email: 'existing@example.com' }; |
| 70 | userRepository.findByEmail.mockResolvedValue({ id: '1', name: 'Existing', email: 'existing@example.com' }); |
| 71 | |
| 72 | await expect(userService.create(createData)).rejects.toThrow('Email already exists'); |
| 73 | }); |
| 74 | }); |
| 75 | }); |
| 76 | ``` |
| 77 | |
| 78 | ### 业务逻辑测试 |
| 79 | |
| 80 | ```typescript |
| 81 | // services/orderService.test.ts |
| 82 | describe('OrderService', () => { |
| 83 | describe('calculateTotal', () => { |
| 84 | it('calculates total with discount', () => { |
| 85 | const items = [ |
| 86 | { price: 100, quantity: 2 }, |
| 87 | { price: 50, quantity: 1 }, |
| 88 | ]; |
| 89 | const discount = 0.1; // 10% off |
| 90 | |
| 91 | const total = orderService.calculateTotal(items, discount); |
| 92 | |
| 93 | expect(total).toBe(225); // (200 + 50) * 0.9 |
| 94 | }); |
| 95 | |
| 96 | it('handles zero discount', () => { |
| 97 | const items = [{ price: 100, quantity: 1 }]; |
| 98 | const total = orderService.calculateTotal(items, 0); |
| 99 | expect(total).toBe(100); |
| 100 | }); |
| 101 | }); |
| 102 | |
| 103 | describe('validateOrder', () => { |
| 104 | it('validates order with sufficient stock', async () => { |
| 105 | const order = { productId: '1', quantity: 5 }; |
| 106 | productRepository.findById.mockResolvedValue({ id: '1', stock: 10 }); |
| 107 | |
| 108 | const result = await orderService.validateOrder(order); |
| 109 | |
| 110 | expect(result.valid).toBe(true); |
| 111 | }); |
| 112 | |
| 113 | it('rejects order with insufficient stock', async () => { |
| 114 | const order = { productId: '1', quantity: 15 }; |
| 115 | productRepository.findById.mockResolvedValue({ id: '1', stock: 10 }); |
| 116 | |
| 117 | const result = await orderService.validateOrder(order); |
| 118 | |
| 119 | expect(result.valid).toBe(false); |
| 120 | expect(result.error).toBe('Insufficient stock'); |
| 121 | }); |
| 122 | }); |
| 123 | }); |
| 124 | ``` |
| 125 | |
| 126 | ## 集成测试编写 |
| 127 | |
| 128 | ### API 端点测试 |
| 129 | |
| 130 | ```typescript |
| 131 | // controllers/userController.test.ts |
| 132 | import request from 'supertest'; |
| 133 | import { app } from '../app'; |
| 134 | import { db } from '../db'; |
| 135 | |
| 136 | describe('User API', () => { |
| 137 | beforeEach(async () => { |
| 138 | // 清理测试数据库 |
| 139 | await db.user.deleteMany(); |
| 140 | }); |
| 141 | |
| 142 | afterAll(async () => { |
| 143 | await db.$disconnect(); |
| 144 | }); |
| 145 | |
| 146 | describe('POST /api/users', () => { |
| 147 | it('creates a new user', async () => { |
| 148 | const userData = { |
| 149 | name: 'Alice', |
| 150 | email: 'alice@example.com', |
| 151 | }; |
| 152 | |
| 153 | const response = await request(app) |
| 154 | .post('/api/users') |
| 155 | .send(userData) |
| 156 | .expect(201); |
| 157 | |
| 158 | expect(response.body.success).toBe(true); |
| 159 | expect(response.body.data).toMatchObject(userData); |
| 160 | expect(response.body.data.id).toBeDefined(); |
| 161 | }); |
| 162 | |
| 163 | it('returns 400 for invalid email', async () => { |
| 164 | const userData = { |
| 165 | name: 'Bob', |
| 166 | email: 'invalid-email', |
| 167 | }; |
| 168 | |
| 169 | const response = await request(app) |
| 170 | .post('/api/users') |
| 171 | .send(userData) |
| 172 | .expect(400); |
| 173 | |
| 174 | expect(response.body.success).toBe(false); |
| 175 | expect(response.body.error.code).toBe('VALIDATION_ERROR'); |
| 176 | }); |
| 177 | |
| 178 | it('r |