Files
skillhub-052-fullstack-dev/references/testing-strategy.md
T
2026-07-13 21:35:57 +08:00

12 KiB
Raw Blame History

# 后端测试策略

面向后端与全栈应用的全面测试指南。涵盖完整的测试金字塔,重点深入 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 信封格式 |

### TypeScriptJest + 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');
  });
});

PythonPytest + FastAPI TestClient

@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 流水线、完整集成测试

事务回滚(推荐默认方式):

let tx: Transaction;
beforeEach(async () => { tx = await db.beginTransaction(); });
afterEach(async () => { await tx.rollback(); });

Docker 测试容器(CI):

# docker-compose.test.yml
services:
  test-db:
    image: postgres:16-alpine
    tmpfs: /var/lib/postgresql/data   # RAM 磁盘以提升速度
    environment:
      POSTGRES_DB: myapp_test

测试工厂(而非原始 SQL

// factories/user.factory.ts
import { faker } from '@faker-js/faker';

export function buildUser(overrides: Partial<User> = {}): 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) });
}
# 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 级别模拟(而非函数级别模拟)

TypeScriptnock):

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');
});

Pythonresponses):

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"

用于基础设施的测试容器

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):

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 中验证:

await new Verifier({
  providerBaseUrl: 'http://localhost:3001',
  pactBrokerUrl: process.env.PACT_BROKER_URL,
  provider: 'UserService',
}).verifyProvider();

5. 性能测试(中优先级)

k6 负载测试

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:「测试单独通过,一起运行时失败」

原因: 测试之间共享了数据库状态。缺少清理操作。

修复方法:

beforeEach(async () => { await db.raw('TRUNCATE orders, users CASCADE'); });
// 或者每次测试使用事务回滚

问题 2:「测试运行结束后 Jest 没有在一秒内退出」

原因: 数据库连接或 HTTP 服务器未关闭。

修复方法:

afterAll(async () => {
  await db.destroy();
  await server.close();
});

问题 3:「异步回调未在超时时间内被调用」

原因: 缺少 async/await 或未处理的 promise。

修复方法:

// ❌ Promise 未被等待
it('should work', () => { request(app).get('/users'); });

// ✅ 正确等待
it('should work', async () => { await request(app).get('/users'); });

问题 4:「集成测试在 CI 中太慢」

修复方法:

  1. 对 PostgreSQL 数据目录使用 tmpfsRAM 磁盘)
  2. beforeAll 中运行一次迁移,在 beforeEach 中截断表
  3. 使用 --maxWorkers 并行化测试套件
  4. 特性分支跳过性能测试(仅在主分支运行)