Skip to content
返回质量保障

集成测试

测试多个模块如何协同工作,在关键处包含真实数据库和 API。

为什么

单元测试证明每个函数单独工作,但大多数 bug 藏在接缝处——数据库返回的格式、API 期望的形状、回滚不正确的事务。集成测试演练模块间的真实接线,抓住 mock 单元测试看不到的假设不匹配。一个在单元测试中返回 {id: 1} 但从真实数据库返回 {userId: 1} 的服务会通过所有单元测试然后在生产挂掉。集成测试是在部署前抓住契约漂移最便宜的方式。

何时使用

在单元测试通过之后、端到端测试之前。每个 API 端点一条,每个数据库仓储方法一条,每个跨模块工作流一条。每个 PR 跑(不是每次保存——它们更慢)。

如何做

1. 选择要跨的边界
   - API handler → service → repository → database(完整垂直切片)
   - 服务 A → 消息队列 → 服务 B(异步集成)
   - 模块 → 第三方 API(用沙箱或录制的响应)

2. 在测试环境起真实依赖
   - 数据库:容器化实例(Testcontainers、docker-compose)
   - 队列:内存 broker 或一次性容器
   - 第三方:录一次真实响应再回放(如 nock、VCR)

3. 种入已知数据,跑流程,断言可观测结果
   - 插入已知字段的用户,调端点,查数据库
   - 断言持久化状态,而非仅 HTTP 响应体
   - 测试间清理(truncate 表、删临时队列)

4. 保持独立和可并行
   - 每个测试创建自己的数据;不用会变的共享 fixture
   - 每个测试用唯一 ID 或命名空间,并行跑不冲突

5. CI 中每个 PR 跑,不是每次保存
   - 与单元测试分开标记(pytest -m integration)
   - 给套件设时限;超 5 分钟就查慢测试

示例

# tests/integration/test_create_order.py
import pytest
from httpx import AsyncClient
from app.main import app
from app.db import get_session

@pytest.mark.integration
async def test_创建订单_持久化并扣库存():
    async with get_session() as session:
        # 种入库存为 5 的商品
        await session.execute("INSERT INTO products (id, stock) VALUES (42, 5)")

    async with AsyncClient(app=app) as client:
        resp = await client.post("/orders", json={"product_id": 42, "qty": 2})

    assert resp.status_code == 201
    order_id = resp.json()["id"]

    # 验证订单已持久化
    async with get_session() as session:
        order = await session.fetch_one("SELECT * FROM orders WHERE id = $1", order_id)
        assert order["qty"] == 2
        product = await session.fetch_one("SELECT stock FROM products WHERE id = 42")
        assert product["stock"] == 3  # 5 - 2 = 3

常见问题

  • ⚠所有集成测试共用一个数据库导致级联失败——一个测试留下脏数据,后面十个测试因与自身逻辑无关的原因挂掉,根因极难找。
  • ⚠在集成测试里 mock 数据库完全违背初衷——你退回到测一个和真 Postgres/MySQL 行为不同的假实现,你想抓的契约漂移反而溜了。
  • ⚠每条 30+ 秒的集成测试会被跳过或标可选,然后它们本该抓的回归出现在 staging 或生产。
  • ⚠只断言 HTTP 响应而不查数据库意味着 handler 可以返回 200 但静默不持久化,测试照样绿。

最佳实践

  • ✓给每个集成测试自己的数据库 schema 或可回滚事务,使测试隔离且可并行不冲突。
  • ✓用 Testcontainers 或 docker-compose override 每次 CI 起一个真数据库——几秒启动值得换来真引擎行为。
  • ✓除了 API 响应还断言持久化状态(直接查库),因为响应可能谎报实际存了什么。
  • ✓集成测试与单元测试分开标记,本地保存时跑快套件,CI 每个 PR 跑完整套件。