软件项目开发全流程 · 第 21 课 · 集成测试与端到端测试
1、"每个零件都对,装起来不转"
工程师把发动机、变速箱、轮子都单独测过,各个正常。可组装成一辆车,开不动——因为接口对不上、时序错了。
软件也一样:单元测试全绿,一集成全是坑。本课讲金字塔的中间层(集成测试) 和顶层(端到端测试),以及它们最容易踩的坑。
2、为什么单元测试不够
单元测试故意隔离了依赖(用 Mock/Fake)。这带来两个盲区:
- 接口不一致:单元里 Mock 说"返回
{id, name}",真实服务返回{user_id, username}——单元绿,集成崩。 - 协作逻辑错误: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) == 17、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。
- 写 3 个集成测试:a) 建任务后能从库里查到;b) 标记完成后状态持久化;c) 删任务连带清理关联记录。
- 写契约测试:为"任务 API"定义 OpenAPI schema,CI 里校验前后端契约一致。
- 写 3 条 E2E 黄金路径(用 Playwright):注册→登录、建任务→标记完成、搜索任务。
- 制造 Flaky 并修复:先用
sleep写一条不稳的,改成智能等待,对比前后。 - 测试数据策略:给上面的测试选一种数据管理方式,写出"造数据 / 清理"的 fixture。
- 反思:哪些场景你故意不写 E2E?为什么?省下的成本换来了什么?
下一课:缺陷管理:Bug 生命周期。
9、今天你带走的三句话
- 集成测试验证"拼装",专治 Mock 掩盖的接口错位与事务问题。
- E2E 只守关键路径(5~15 条),别拿它穷举分支。
- Flaky 是信任杀手——智能等待 + 稳定钩子 + 隔离数据,且 Flaky 要当 bug 修。
下一课:缺陷管理:Bug 生命周期。