1、"每个零件都对,装起来不转"

工程师把发动机、变速箱、轮子都单独测过,各个正常。可组装成一辆车,开不动——因为接口对不上、时序错了。

软件也一样:单元测试全绿,一集成全是坑。本课讲金字塔的中间层(集成测试) 和顶层(端到端测试),以及它们最容易踩的坑。

2、为什么单元测试不够

单元测试故意隔离了依赖(用 Mock/Fake)。这带来两个盲区:

  1. 接口不一致:单元里 Mock 说"返回 {id, name}",真实服务返回 {user_id, username}——单元绿,集成崩。
  2. 协作逻辑错误:A 写完数据没提交事务,B 读不到——各自单元都对,合起来错。

集成测试正是补这个盲区:让真实组件一起工作,验证"拼装正确"。

3、集成测试测什么

集成点测什么例子
持久层数据真的读/写得对"创建订单后能从库里查到"
服务间调用约定、错误处理"库存服务超时,订单服务降级"
外部依赖第三方 SDK 用对没"支付回调签名验证通过"
事务/并发一致性"并发下单不超卖"
import pytest

@pytest.fixture
def db():
    # 用真实(或接近真实)的数据库,测试后清理
    conn = connect("sqlite:///:memory:")
    create_tables(conn)
    yield conn
    conn.close()

def test_create_order_persists_to_db(db):
    order = create_order(db, user_id=1, items=[("book", 2)])
    row = db.execute("SELECT * FROM orders WHERE id=?", order.id).fetchone()
    assert row is not None
    assert row["user_id"] == 1

工具提示:

  • 用 Testcontainers / Docker 起真实的 Postgres、Redis,比用 SQLite 假库更能发现方言差异。
  • 每个测试独立准备数据 + 结束清理,避免测试间互相污染。

4、接口契约测试

当两个服务(或前后端)联调时,最容易出问题的是"我以为你会返回 X,你却返回了 Y"。

契约测试(Contract Testing) 的思路:

消费者(调用方)写一个「契约」:我需要 GET /user/1 → {id, name}
提供者(被调方)跑契约测试:我确实能返回这个格式吗?
        ↓
双向验证,任何一方破坏契约,立刻红

工具:Pact、Spring Cloud Contract。

更轻量的替代:OpenAPI / JSON Schema——把接口格式写成机器可校验的规格,CI 里校验"实现是否符合 schema"(第 9 课的接口契约落地)。

价值:契约测试让"联调"从"上线前的手忙脚乱"变成"CI 里的自动红灯"。

5、端到端测试:模拟真实用户

E2E 测试从用户视角走完整流程,通常驱动真实浏览器:

# 用 Playwright
def test_user_can_checkout(page):
    page.goto("https://shop.example.com")
    page.click("text=加入购物车")
    page.click("text=去结算")
    page.fill("#address", "北京市……")
    page.click("text=确认支付")
    assert page.text_content(".order-status") == "支付成功"

E2E 的定位:

  • ✅ 守关键路径:登录、下单、支付、注册——这些挂了业务就停摆。
  • ✅ 验证跨系统:前端 + 后端 + 数据库 + 第三方,全链路。
  • ❌ 别拿它测所有分支:慢、脆、贵。一条分支测所有组合,维护成本爆炸。

原则:E2E 只跑 5~15 条"黄金路径",其余交给下层测试。

6、测试数据管理:最容易被忽视的坑

集成/E2E 测试常需要数据,处理不好就互相干扰:

策略做法优劣
每个测试自建自清测试内造数据、跑完删干净、独立,但慢
快照回滚测前存快照,测后恢复快,需数据库支持
事务回滚测试包在事务里,结束回滚快,但测不了跨事务逻辑
固定种子数据每次灌同一批数据可复现,但测试间可能耦合

最佳实践:每个测试自建自清 + 唯一标识(如用随机 ID),保证可重复、可并行。

def test_search_finds_only_my_tasks(db):
    uid = uuid4().hex              # 唯一标识,避免撞车
    create_task(db, title=f"task-{uid}")
    results = search_tasks(db, f"task-{uid}")
    assert len(results) == 1

7、E2E 的头号敌人:Flaky(时好时坏)

Flaky 测试:同样的代码,有时绿有时红。它比"全红"更可怕——因为它摧毁信任,团队会开始"重跑一下就好",最后无视所有红灯。

常见原因与对策:

原因对策
用了固定 sleep,元素还没加载用智能等待(等待元素出现/可点击),别用固定 sleep
元素定位太脆(依赖深层 CSS)用 data-testid 等稳定的测试钩子
测试间数据互相污染唯一数据、测后清理
并发/异步时序不定显式等待状态,避免竞态
动画/时间因素关动画、冻结时间(fake clock)
# ❌ 脆:睡 2 秒,网络慢就挂
time.sleep(2)
# ✅ 稳:等元素真的可点
page.wait_for_selector("#submit", state="visible")
纪律:Flaky 测试要当 bug 修,不许"重跑掩盖"。否则金字塔顶层就成了摆设。

8、动手实验 🎯

任务:给贯穿项目「团队任务清单」补集成测试 + 关键路径 E2E。

  1. 写 3 个集成测试:a) 建任务后能从库里查到;b) 标记完成后状态持久化;c) 删任务连带清理关联记录。
  2. 写契约测试:为"任务 API"定义 OpenAPI schema,CI 里校验前后端契约一致。
  3. 写 3 条 E2E 黄金路径(用 Playwright):注册→登录、建任务→标记完成、搜索任务。
  4. 制造 Flaky 并修复:先用 sleep 写一条不稳的,改成智能等待,对比前后。
  5. 测试数据策略:给上面的测试选一种数据管理方式,写出"造数据 / 清理"的 fixture。
  6. 反思:哪些场景你故意不写 E2E?为什么?省下的成本换来了什么?

下一课:缺陷管理:Bug 生命周期。

9、今天你带走的三句话

  1. 集成测试验证"拼装",专治 Mock 掩盖的接口错位与事务问题。
  2. E2E 只守关键路径(5~15 条),别拿它穷举分支。
  3. Flaky 是信任杀手——智能等待 + 稳定钩子 + 隔离数据,且 Flaky 要当 bug 修。

下一课:缺陷管理:Bug 生命周期。

标签: 软件工程, 项目管理, 开发流程

添加新评论