```markdown # 后端测试策略 面向后端与全栈应用的全面测试指南。涵盖完整的测试金字塔,重点深入 API 集成测试、数据库测试、契约测试与性能测试。 ## 快速启动清单 - [ ] **测试运行器已配置**(Jest/Vitest、Pytest、Go test) - [ ] **测试数据库**就绪(Docker 容器或内存数据库) - [ ] **每次测试的数据库隔离**(事务回滚或截断表) - [ ] **测试工厂**用于常见实体(用户、订单、商品) - [ ] **认证助手**用于生成测试令牌 - [ ] **CI 流水线**使用真实数据库服务运行测试 - [ ] **覆盖率阈值**已设定(≥ 80%) --- ## 测试金字塔 ``` ╱╲ E2E(少量、慢速)——跨服务的完整流程 ╱ ╲ ╱────╲ 集成测试(适中)——API + 数据库 + 外部服务 ╱ ╲ ╱────────╲ 单元测试(大量、快速)——纯业务逻辑 ╱__________╲ ``` | 层级 | 测试内容 | 速度 | 数量 | |-------|------|-------|-------| | 单元测试 | 纯函数、业务逻辑、无 I/O | < 10ms | 占总测试的 70% 以上 | | 集成测试 | API 路由 + 真实数据库 + 模拟外部服务 | 50-500ms | 约 20% | | E2E 测试 | 跨已部署服务的完整用户流程 | 1-30s | 约 10% | | 契约测试 | 服务间的 API 兼容性 | < 100ms | 每个 API 边界 | | 性能测试 | 负载、压力、浸泡测试 | 数分钟 | 每个关键路径 | --- ## 1. API 集成测试(关键) ### 每个端点需要测试的内容 | 方面 | 需编写的测试 | |--------|---------------| | 正常路径 | 正确的输入 → 预期的响应 + 正确的数据库状态 | | 认证 | 无令牌 → 401,错误令牌 → 401,过期令牌 → 401 | | 授权 | 错误角色 → 403,非所有者 → 403 | | 校验 | 缺少字段 → 422,错误类型 → 422,边界值 | | 未找到 | 无效 ID → 404,已删除资源 → 404 | | 冲突 | 重复创建 → 409,过期更新 → 409 | | 幂等性 | 相同请求两次 → 相同结果 | | 副作用 | 数据库状态变更、事件被触发、缓存失效 | | 错误格式 | 所有错误均符合 RFC 9457 信封格式 | ### TypeScript(Jest + Supertest) ```typescript describe('POST /api/orders', () => { let token: string; let product: Product; beforeAll(async () => { await resetDatabase(); const user = await createTestUser({ role: 'customer' }); token = await getAuthToken(user); product = await createTestProduct({ price: 29.99, stock: 10 }); }); it('创建订单 → 201 + 正确的数据库状态', async () => { const res = await request(app) .post('/api/orders') .set('Authorization', `Bearer ${token}`) .send({ items: [{ productId: product.id, quantity: 2 }] }); expect(res.status).toBe(201); expect(res.body.data.total).toBe(59.98); const updated = await db.product.findUnique({ where: { id: product.id } }); expect(updated!.stock).toBe(8); }); it('无认证时拒绝 → 401', async () => { const res = await request(app).post('/api/orders').send({ items: [] }); expect(res.status).toBe(401); }); it('空商品列表时拒绝 → 422', async () => { const res = await request(app) .post('/api/orders') .set('Authorization', `Bearer ${token}`) .send({ items: [] }); expect(res.status).toBe(422); expect(res.body.errors[0].field).toBe('items'); }); }); ``` ### Python(Pytest + FastAPI TestClient) ```python @pytest.fixture def client(db_session): def override_get_db(): yield db_session app.dependency_overrides[get_db] = override_get_db yield TestClient(app) app.dependency_overrides.clear() def test_create_order_success(client, auth_headers, test_product): response = client.post("/api/orders", json={ "items": [{"product_id": test_product.id, "quantity": 2}] }, headers=auth_headers) assert response.status_code == 201 assert response.json()["data"]["total"] == 59.98 def test_create_order_no_auth(client): response = client.post("/api/orders", json={"items": []}) assert response.status_code == 401 def test_create_order_empty_items(client, auth_headers): response = client.post("/api/orders", json={"items": []}, headers=auth_headers) assert response.status_code == 422 ``` --- ## 2. 数据库测试(高优先级) ### 测试隔离策略 | 策略 | 速度 | 真实度 | 使用时机 | |----------|-------|---------|------| | **事务回滚** | ⚡ 最快 | 中等 | 单元测试 + 集成测试的默认方式 | | **截断表** | 快 | 高 | 事务回滚不可用时 | | **测试容器** | 启动慢 | 最高 | CI 流水线、完整集成测试 | **事务回滚(推荐默认方式):** ```typescript let tx: Transaction; beforeEach(async () => { tx = await db.beginTransaction(); }); afterEach(async () => { await tx.rollback(); }); ``` **Docker 测试容器(CI):** ```yaml # docker-compose.test.yml services: test-db: image: postgres:16-alpine tmpfs: /var/lib/postgresql/data # RAM 磁盘以提升速度 environment: POSTGRES_DB: myapp_test ``` ### 测试工厂(而非原始 SQL) ```typescript // factories/user.factory.ts import { faker } from '@faker-js/faker'; export function buildUser(overrides: Partial = {}): CreateUserDTO { return { email: faker.internet.email(), firstName: faker.person.firstName(), role: 'customer', ...overrides, }; } export async function createUser(overrides = {}) { return db.user.create({ data: buildUser(overrides) }); } ``` ```python # factories/user_factory.py import factory from faker import Faker class UserFactory(factory.Factory): class Meta: model = User email = factory.LazyAttribute(lambda _: Faker().email()) first_name = factory.LazyAttribute(lambda _: Faker().first_name()) role = "customer" ``` --- ## 3. 外部服务测试(高优先级) ### HTTP 级别模拟(而非函数级别模拟) **TypeScript(nock):** ```typescript import nock from 'nock'; it('成功处理支付', async () => { nock('https://api.stripe.com') .post('/v1/charges') .reply(200, { id: 'ch_123', status: 'succeeded', amount: 5000 }); const result = await paymentService.charge({ amount: 50.00, currency: 'usd' }); expect(result.status).toBe('succeeded'); }); it('处理支付超时', async () => { nock('https://api.stripe.com').post('/v1/charges').delay(10000).reply(200); await expect(paymentService.charge({ amount: 50, currency: 'usd' })) .rejects.toThrow('timeout'); }); ``` **Python(responses):** ```python import responses @responses.activate def test_payment_success(): responses.post("https://api.stripe.com/v1/charges", json={"id": "ch_123", "status": "succeeded"}, status=200) result = payment_service.charge(amount=50.00, currency="usd") assert result.status == "succeeded" ``` ### 用于基础设施的测试容器 ```typescript import { PostgreSqlContainer } from '@testcontainers/postgresql'; import { RedisContainer } from '@testcontainers/redis'; beforeAll(async () => { const pg = await new PostgreSqlContainer('postgres:16').start(); process.env.DATABASE_URL = pg.getConnectionUri(); await runMigrations(); }, 60000); ``` --- ## 4. 契约测试(中高优先级) ### 消费者驱动的契约(Pact) **消费者(OrderService 调用 UserService):** ```typescript it('可以根据 ID 获取用户', async () => { await pact.addInteraction() .given('用户 usr_123 存在') .uponReceiving('GET /users/usr_123') .withRequest('GET', '/api/users/usr_123') .willRespondWith(200, (b) => { b.jsonBody({ data: { id: MatchersV3.string(), email: MatchersV3.email() } }); }) .executeTest(async (mockserver) => { const user = await new UserClient(mockserver.url).getUser('usr_123'); expect(user.id).toBeDefined(); }); }); ``` **提供者在 CI 中验证:** ```typescript await new Verifier({ providerBaseUrl: 'http://localhost:3001', pactBrokerUrl: process.env.PACT_BROKER_URL, provider: 'UserService', }).verifyProvider(); ``` --- ## 5. 性能测试(中优先级) ### k6 负载测试 ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '30s', target: 20 }, // 逐步增加 { duration: '1m', target: 100 }, // 持续维持 { duration: '30s', target: 0 }, // 逐步减少 ], thresholds: { http_req_duration: ['p(95)<500', 'p(99)<1000'], http_req_failed: ['rate<0.01'], }, }; export default function () { const res = http.get(`${__ENV.BASE_URL}/api/orders`); check(res, { 'status 200': (r) => r.status === 200 }); sleep(1); } ``` ### 性能预算 | 指标 | 目标值 | 超出后措施 | |--------|--------|--------------------| | p95 响应时间 | < 500ms | 优化查询/缓存 | | p99 响应时间 | < 1000ms | 检查异常查询 | | 错误率 | < 0.1% | 排查异常尖峰 | | 数据库查询时间 | 每次 < 100ms | 添加索引 | ### 何时运行 | 触发时机 | 测试类型 | |---------|-----------| | 重大版本发布前 | 完整负载测试 | | 新增数据库查询/索引 | 查询基准测试 | | 基础设施变更 | 基线对比 | | 每周(CI) | 冒烟负载测试 | --- ## 测试文件组织 ``` tests/ unit/ # 纯逻辑,模拟依赖 order.service.test.ts integration/ # API + 真实数据库 orders.api.test.ts auth.api.test.ts contracts/ # 消费者驱动的契约 user-service.consumer.pact.ts performance/ # 负载测试 load-test.js fixtures/ factories/ # 测试数据工厂 user.factory.ts seeds/ test-data.ts helpers/ setup.ts # 全局测试配置 auth.helper.ts # 令牌生成 db.helper.ts # 数据库清理 ``` --- ## 反模式 | # | ❌ 不要这样做 | ✅ 应该这样做 | |---|---------|--------------| | 1 | 仅测试正常路径 | 测试错误、认证、校验、边界情况 | | 2 | 什么都模拟(无真实数据库) | 使用测试容器或测试数据库 | | 3 | 测试依赖执行顺序 | 每个测试自行设置/清理自身状态 | | 4 | 硬编码测试数据 | 使用工厂(faker + 覆盖参数) | | 5 | 测试实现细节 | 测试行为:输入 → 输出 | | 6 | 共享可变状态 | 每次测试隔离(事务回滚) | | 7 | 在 CI 中跳过迁移测试 | 在 CI 中从头运行迁移 | | 8 | 发布前不做性能测试 | 每个重大版本都做负载测试 | | 9 | 对生产数据做测试 | 仅使用生成的测试数据 | | 10 | 测试套件超过 10 分钟 | 并行化、使用 RAM 磁盘、优化设置 | --- ## 常见问题 ### 问题 1:「测试单独通过,一起运行时失败」 **原因:** 测试之间共享了数据库状态。缺少清理操作。 **修复方法:** ```typescript beforeEach(async () => { await db.raw('TRUNCATE orders, users CASCADE'); }); // 或者每次测试使用事务回滚 ``` ### 问题 2:「测试运行结束后 Jest 没有在一秒内退出」 **原因:** 数据库连接或 HTTP 服务器未关闭。 **修复方法:** ```typescript afterAll(async () => { await db.destroy(); await server.close(); }); ``` ### 问题 3:「异步回调未在超时时间内被调用」 **原因:** 缺少 `async/await` 或未处理的 promise。 **修复方法:** ```typescript // ❌ Promise 未被等待 it('should work', () => { request(app).get('/users'); }); // ✅ 正确等待 it('should work', async () => { await request(app).get('/users'); }); ``` ### 问题 4:「集成测试在 CI 中太慢」 **修复方法:** 1. 对 PostgreSQL 数据目录使用 `tmpfs`(RAM 磁盘) 2. 在 `beforeAll` 中运行一次迁移,在 `beforeEach` 中截断表 3. 使用 `--maxWorkers` 并行化测试套件 4. 特性分支跳过性能测试(仅在主分支运行) ```