跳转至

测试成为开发者的日常 — 从单元测试到持续反馈环

这一章讲什么: 测试不再是写完代码之后交给 QA 的事,而是开发者每改一行代码 就立刻发生一次的本能。本章从最小的单元测试讲起,一层层走到 CI/CD 流水线里的 全套质量门,落点是全章反复敲的一句话:反馈越晚,修复越贵;让反馈早到,事故就少。 原书给了一个反直觉但证据充分的案例:把覆盖率从 90% 主动降到 75%,事故反而少了六成。

1. 主线例子:一次转账要经过的几层测试

本章的主线例子,是微服务银行系统里的一次转账,看它被几种测试各摸一遍。

系统由四个服务组成:用户账户、支付、通知、欺诈检测。你改了支付服务里的一行汇率取整逻辑:

  第 1 层 单元测试(秒级,本地):
    输入 100.005 元,断言取整结果是 100.01 而不是 100.00
    → 毫秒级返回,不碰数据库、不碰网络
  第 2 层 集成测试(分钟级,CI 里的容器环境):
    支付服务真的去调通知服务,断言「转账成功后用户收到一条回执」
    → 测的是两个真实服务之间的接口对不对得上
  第 3 层 E2E 测试(更慢,预发环境):
    模拟一个真实用户:登录 → 转钱 → 收到通知
    → 测的是整条用户旅程,包括浏览器界面
  第 4 层 部署后冒烟(生产):
    上线后自动打一笔探测转账,确认接口响应在正常延迟内

  图说:同一次转账,四种测试各回答一个问题。越往上越像真实世界,越慢、越脆、数量越少。
  服务构成与用例沿原书银行微服务例子;具体金额是为演示编的。

这四层就是本章的全部结构:§2 讲第 1 层(单元测试怎么写才对),§3 讲「测得全」和「测得对」的区别 (覆盖率),§4 讲第 2、3 层的分工(集成 vs E2E,测试金字塔),§5 讲把它们嵌进流水线 (持续测试),§6 讲落点(早反馈为什么省钱)。

2. 单元测试:最小、最快、最多

单元测试验证的是应用里最小的可测单元——一个函数、方法或类——并且不依赖外部系统 (数据库、API、文件系统都用假的替代)1。

原书的承重例子是一个算折扣的函数,配套三条测试(照抄结构):有效折扣 (calculate_discount(100, 10) == 90)、零折扣(价格不变)、非法输入 (百分比 150 抛异常)2。

2.1 四大原则(原书的清单,接住)

原则 含义
隔离 外部依赖一律用 mock/stub(假对象)替代,不做真实网络调用
可重复 同输入必同输出;不依赖系统时间、随机数,除非显式 mock
快 毫秒级跑完,因为它要在每次改代码时都跑
清晰 好测试像文档,读测试名就知道在验证什么行为

2.2 AAA 模式与七条写作纪律

写测试的身体动作是 AAA:Arrange(摆好输入)→ Act(调用被测函数)→ Assert(断言结果)3。 配套纪律:一个测试只盖一个场景;测试目录镜像源码目录(src/payments/ 对 tests/payments/);命名说行为不说实现(test_login_success 而不是 test_function_1);测试代码也要过代码评审;边界条件、非法输入、异常路径必测4。

2.3 好测试 vs 坏测试(原书表 5.1)

维度 好测试 坏测试 为什么要紧
隔离 mock 外部依赖 真连数据库和 API 网络抖动变成「测试失败」,CI 全是噪音
速度 毫秒级 秒级到分钟级 慢测试没人跑,反馈环就断了
确定性 结果稳定 时过时挂(flaky, flaky test) 信任崩塌,红灯被当狼来了
范围 只测一个单元 一次塞太多功能 挂了不知道是哪件事坏了
独立性 任意顺序可跑 依赖前一个测试留下的状态 无法并行,CI 流水线被拖死

坏测试的危害书里点得很准:一个依赖真实数据库的测试,会因为连不上网而失败, 误导开发者以为代码有缺陷——噪音消耗的是对整个自动化体系的信任5。

3. 覆盖率:75% 的有意义覆盖 > 90% 的水分覆盖

代码覆盖率衡量测试执行时覆盖了源码的百分之多少。先要分清四种覆盖(名字留)6:

类型 衡量什么
行覆盖 多少比例的代码行被执行过
分支覆盖 每个 if/else 的两个分支是否都走过
函数覆盖 每个函数是否被调用过
条件覆盖 每个布尔子表达式是否都取过真和假

行覆盖会撒谎,分支覆盖拆穿它。 原书的例子是一个打分的函数:>=90 给 A、>=75 给 B、 >=50 给 C、其余给 F。只测 95 和 30 两个输入,行覆盖 100%——但 B、C 两个分支从来没执行过7。

所以原书给的目标不是 100%,而是:70–80%,火力集中在关键路径——金融计算、认证流程、 数据处理模块必须测满;并且断言要验证输出和异常,不是只让代码「跑过」8。

案例:电商结账服务的覆盖率的真相

这是本章最值钱的案例(无出处,但结构典型)9:

  某电商结账微服务,目标 90% 覆盖率,达标了,但生产事故照出:
    · 折扣码逻辑只测了「有效券」——过期券、非法券没人测
    · 支付网关只测「成功交易」——拒付、余额不足、网络超时没人模拟
    · 税率计算只测标准税率——某地区的特殊免税场景没人测
  → 90% 覆盖率的假象下,大促期间购物车连环故障

  转向「有意义覆盖」之后:
    · 补边界:非法输入、拒付、高并发
    · 补负向测试:错误处理是否正确
    · 引入分支覆盖,逼每个决策点都走过
  → 覆盖率从 90% 降到 75%(删掉了冗余的重复 happy path 测试)
  → 支付与折扣相关的生产事故下降 60%

  图说:删测试反而更可靠——删的是水分,留的是防线。

判断(我们的,不是书里的): 这个案例是全书少数「反指标崇拜」的清醒时刻, 它点破了一件事:覆盖率衡量的是「代码被执行」,不是「行为被验证」。 把覆盖率当 KPI 的团队,得到的自然是针对 KPI 优化的测试——专挑好测的路径铺量。 真正该度量的,是「关键业务路径上,哪些输入组合(含失败路径)有断言」。 如果错,会错在: 在低风险的代码库里,高覆盖率与高质量测试的相关性其实不错, 「反对覆盖率崇拜」在低风险场景会显得过度矫正;这个判断适用于关键路径密集的系统。

4. 集成测试与 E2E:分工、代价与测试金字塔

4.1 各自回答什么问题

集成测试验证多个组件组合起来是否协同:登录流程涉及认证 API、用户数据库、 会话服务三个组件,各自单元测试全绿,组合起来仍可能因为「数据库返回的字段格式 和 API 预期差了一点」而失败——集成测试抓的就是这种接缝处的不匹配10。

E2E 测试(端到端)再进一步:从用户视角模拟完整流程——登录、加购、用券、 支付、收到确认。它验证的是「所有服务、API、界面合起来交付了预期的客户体验」11。

银行例子的对照(原书原例)12:

层 测什么(银行转账场景)
单元 支付计算逻辑、欺诈规则
集成 支付服务能否成功调通知服务发出回执
E2E 真实用户登录→转账→收到通知的完整旅程
冒烟 部署后转账 API 响应延迟是否可接受

4.2 代价与三大挑战

E2E 的真实代价:慢、脆、环境复杂。三大挑战——flaky(时序、网络抖动导致时过时挂)、 环境搭建复杂(要编排一堆服务)、执行慢(拖慢反馈)13。

解法就是测试金字塔(testing pyramid,名字必须留):大量快速单元测试打底, 中等数量的集成测试验契约,只有极少数 E2E 测业务关键流(结账、登录、订阅续费)14。

        ╱  E2E        ╲   极少,只测业务关键旅程(慢、脆、贵)
       ╱  集成测试     ╲  中等,验证服务间契约
      ╱  单元测试       ╲ 最多,毫秒级,验证单点逻辑
     ╱__________________╲

  图说:金字塔的力学——底座越大,塔尖才立得稳;全靠塔尖(全 E2E)的测试体系必然又慢又脆。

配套最佳实践:集成测试测服务间契约(用 Pact 这类契约测试框架,名字留), 用带种子数据的专用测试数据库,环境用 Docker/IaC 自动化搭建; E2E 只测用户关键流,用 Selenium/Cypress/Playwright(名字留)驱动浏览器, 错误路径也要测(非法登录、会话过期、网络延迟),且别把单元/集成该干的活塞给 E2E15。

5. 持续测试:流水线里的四层质量门

持续测试不是「跑几个自动脚本」,是一种哲学:测试无处不在、时刻发生、默认开启—— 每次提交、每个合并请求、每次构建,都是验证系统的机会16。

一个典型流水线把测试当渐进的质量门(quality gate),每一层都更快、更靠前的反馈:

门 位置 跑什么 工具(原书点名)
提交前 开发者笔记本 lint、风格检查、快单元测试 pre-commit hooks、ESLint、pylint、husky
CI 阶段 每次提交/PR 单元 + 集成,过了才产出制品 GitHub Actions、Jenkins、GitLab CI
部署前 预发环境 E2E、性能、安全扫描 Selenium、Cypress、Playwright、JMeter、OWASP ZAP
部署后 生产 冒烟测试、金丝雀检查 Prometheus 告警、Datadog、自定义健康检查

原书给了一个可以直接抄的 GitHub Actions 工作流(Python 项目):push 或 PR 触发 → 装依赖 → pytest 跑单元测试 → coverage run -m pytest 出覆盖率报告 → 跑 tests/integration → 上传到 Codecov 看板17。

三个运营难题与解法:flaky 测试(用 mock、重试、一致环境稳住)、流水线瓶颈 (并行化、拆阶段)、环境漂移(容器化、临时环境)18。

案例(流媒体平台,数字无出处):人工回归一次要两周、月更;嵌入连续测试后—— 部署失败降 70%、发布中问题检测时间(MTTD)从 45 分钟压到 10 分钟以内、 发布频率从月更到每日多次、部署后缺陷降 60%19。

6. 早反馈:全章的落点

这一节把「测试为什么能减事故」的机制说透:事故的根源常不是没测试,是反馈太慢—— 开发者太晚才知道,修起来又难又贵20。

成本对照(原书口径):编码阶段发现的缺陷几分钟搞定;同样的缺陷到了生产, 是几小时几天、跨团队、热修复加回滚21。四个让反馈变早的杠杆: 流水线每阶段自动测;左移(把容器漏洞扫描这类检查从部署后挪到构建期); 开发中就能看的本地仪表盘;测试一挂立刻推送(Slack/邮件),坏代码不在共享分支上滞留22。

支付网关案例(机制级,值得记住):一家金融科技公司上线汇率取整功能, 测试只覆盖了 happy path,取整错误的 bug 漏进生产,几小时内几百笔交易失败,被迫回滚。 重构后的防线:单元测试覆盖多种取整方向(含 0.005 这种边界)、集成测试模拟第三方 支付方、E2E 校验回执金额、覆盖率仪表盘在关键模块出现缺口时立刻告警23。

原书最后把反馈环画成一个七段循环:Code → Build → Test → Deploy → Monitor → Feedback → Improve,每段的信号喂给下一段,从监控和事故中学到的教训回到设计, 循环重启24。配套纪律:fail fast(首个关键失败即停)、测试原子化、特性开关灰度放行、 开发者进值班轮(亲尝事故,自然会长出缩短反馈环的习惯)25。

判断(我们的,不是书里的): 本章的「反馈越晚越贵」与第 4 章安全章的 「生产修复贵 100 倍」是同一根轴——两章合起来,这本书对「左移」的完整论证是: 左移不是道德号召,是成本数学。这也解释了为什么测试金字塔的形状由反馈速度决定: 塔尖测试之所以只能有一点点,不是因为它们没价值,而是因为它们慢, 慢反馈在大批量提交面前等于没有反馈。 如果错,会错在: 如果 AI 辅助生成的测试把塔尖测试的编写和维护成本打到接近零, 「E2E 只能有一点点」的配比就要重估——形状变了,力学未必变,但比例会移。

7. 边界与局限

  1. 所有量化数字(70%、60%、45→10 分钟)均无出处,作为量级感知可以,当基准引用不行。
  2. 契约测试只给了 Pact 一个名字,没有展开消费者驱动的契约是什么、怎么落地—— 微服务测试里最尖的接缝问题,本章讲得比标题浅。
  3. 测试金字塔没有给配比数字(单元:集成:E2E 具体几比几),只说「多、中、少」。
  4. 性能测试被一笔带过(只在质量门里出现 JMeter),压测策略完全留给第 14 章。
  5. flaky 测试给了对策但没有根因分类(数据竞争?外部依赖?时序假设?),实操时还需别的材料。

8. 可带走的

  1. 单元测试四原则:隔离(mock 外部)、可重复、毫秒级、清晰如文档;
  2. AAA 结构:摆输入 → 调函数 → 断言;测试命名说行为,代码评审同样审测试;
  3. 覆盖率四种,行覆盖会撒谎,分支覆盖拆穿它;目标 70–80%,火力压关键路径;
  4. 有意义覆盖 > 百分比:删掉冗余 happy path 测试,把省下的力气投进边界与失败路径;
  5. 集成测接缝(契约),E2E 测旅程;金字塔底座必须大,全靠 E2E 的体系又慢又脆;
  6. 流水线四层门:提交前 → CI → 预发 → 生产冒烟;高危信号在越左边的门拦住越省钱;
  7. flaky 测试是信任杀手,宁可修一个flaky,不加十个新测试;
  8. 反馈环七段循环:监控和事故的教训必须流回设计,循环才算闭。

9. 原文地图

主题 原书章 原文位置
单元测试四原则 Introduction(第 5 章) text/23-fm-introduction.txt:74(搜「Isolation」) · text/23-fm-introduction.txt:80(搜「milliseconds」)
折扣函数与 AAA Introduction(第 5 章) text/23-fm-introduction.txt:93(搜「calculate_discount」) · text/23-fm-introduction.txt:120(搜「assertEqual」)
好/坏测试对照表 Introduction(第 5 章) text/23-fm-introduction.txt:318(搜「Aspect」) · text/23-fm-introduction.txt:502(搜「Table 5.1」)
四种覆盖类型与打分例子 Introduction(第 5 章) text/23-fm-introduction.txt:544(搜「Line coverage」) · text/23-fm-introduction.txt:590(搜「B and C branches」)
覆盖率最佳实践(70–80%) Introduction(第 5 章) text/23-fm-introduction.txt:600(搜「70 to 80%」)
电商结账案例(90%→75%,-60%) Introduction(第 5 章) text/23-fm-introduction.txt:629(搜「checkout microservice」) · text/23-fm-introduction.txt:663(搜「60% reduction」)
集成 vs E2E、银行例子 Introduction(第 5 章) text/23-fm-introduction.txt:682(搜「authentication API」) · text/23-fm-introduction.txt:925(搜「transferring funds」)
测试金字塔 Introduction(第 5 章) text/23-fm-introduction.txt:795(搜「testing pyramid strategy」) · text/23-fm-introduction.txt:823(搜「Figure 5.2」)
四层质量门 Introduction(第 5 章) text/23-fm-introduction.txt:861(搜「Pre-commit hooks」) · text/23-fm-introduction.txt:900(搜「Canary checks」)
GitHub Actions 工作流 Introduction(第 5 章) text/23-fm-introduction.txt:970(搜「CI Pipeline」) · text/23-fm-introduction.txt:1038(搜「pytest」)
流媒体平台案例 Introduction(第 5 章) text/23-fm-introduction.txt:1119(搜「monthly releases」) · text/23-fm-introduction.txt:1143(搜「70%」)
支付网关取整事故 Introduction(第 5 章) text/23-fm-introduction.txt:1225(搜「rounding errors」) · text/23-fm-introduction.txt:1232(搜「0.005」)
反馈环七段 Introduction(第 5 章) text/23-fm-introduction.txt:1254(搜「code, build, test」) · text/23-fm-introduction.txt:1288(搜「Fail fast」)

  1. 出处:「Introduction」(第 5 章)第 61 段(text/23-fm-introduction.txt:61,搜「smallest testable part」)。 ↩

  2. 出处:「Introduction」(第 5 章)第 93–155 段(text/23-fm-introduction.txt:93,搜「calculate_discount」;text/23-fm-introduction.txt:120,搜「assertEqual」)。 ↩

  3. 出处:「Introduction」(第 5 章)第 168 段(text/23-fm-introduction.txt:168,搜「arrange, act, assert」)。 ↩

  4. 出处:「Introduction」(第 5 章)第 168–189 段(text/23-fm-introduction.txt:174,搜「Mirror the structure」;text/23-fm-introduction.txt:189,搜「edge cases」)。 ↩

  5. 出处:「Introduction」(第 5 章)第 505 段(text/23-fm-introduction.txt:505,搜「live database」)。 ↩

  6. 出处:「Introduction」(第 5 章)第 543–553 段(text/23-fm-introduction.txt:544,搜「Line coverage」;text/23-fm-introduction.txt:547,搜「Branch coverage」)。 ↩

  7. 出处:「Introduction」(第 5 章)第 563–590 段(text/23-fm-introduction.txt:590,搜「B and C branches」)。 ↩

  8. 出处:「Introduction」(第 5 章)第 600–609 段(text/23-fm-introduction.txt:600,搜「70 to 80%」)。 ↩

  9. 出处:「Introduction」(第 5 章)第 629–666 段(text/23-fm-introduction.txt:663,搜「60% reduction」)。 ↩

  10. 出处:「Introduction」(第 5 章)第 678–692 段(text/23-fm-introduction.txt:692,搜「mismatches」)。 ↩

  11. 出处:「Introduction」(第 5 章)第 718–738 段(text/23-fm-introduction.txt:721,搜「checkout process」)。 ↩

  12. 出处:「Introduction」(第 5 章)第 919–928 段(text/23-fm-introduction.txt:925,搜「transferring funds」)。 ↩

  13. 出处:「Introduction」(第 5 章)第 781–793 段(text/23-fm-introduction.txt:785,搜「Flakiness」)。 ↩

  14. 出处:「Introduction」(第 5 章)第 795–809 段(text/23-fm-introduction.txt:799,搜「many fast unit tests」)。 ↩

  15. 出处:「Introduction」(第 5 章)第 702–757 段(text/23-fm-introduction.txt:702,搜「Pact」;text/23-fm-introduction.txt:748,搜「user-critical flows」)。 ↩

  16. 出处:「Introduction」(第 5 章)第 851 段(text/23-fm-introduction.txt:851,搜「everywhere, all the time」)。 ↩

  17. 出处:「Introduction」(第 5 章)第 970–1073 段(text/23-fm-introduction.txt:1038,搜「pytest」;text/23-fm-introduction.txt:1056,搜「codecov」)。 ↩

  18. 出处:「Introduction」(第 5 章)第 1103–1109 段(text/23-fm-introduction.txt:1103,搜「mocks, retries」;text/23-fm-introduction.txt:1109,搜「ephemeral environments」)。 ↩

  19. 出处:「Introduction」(第 5 章)第 1119–1156 段(text/23-fm-introduction.txt:1143,搜「Deployment failures dropped」;text/23-fm-introduction.txt:1146,搜「45 minutes」)。 ↩

  20. 出处:「Introduction」(第 5 章)第 1165 段(text/23-fm-introduction.txt:1165,搜「delay in receiving feedback」)。 ↩

  21. 出处:「Introduction」(第 5 章)第 1171–1182 段(text/23-fm-introduction.txt:1175,搜「minutes to resolve」)。 ↩

  22. 出处:「Introduction」(第 5 章)第 1188–1216 段(text/23-fm-introduction.txt:1203,搜「Trivy」;text/23-fm-introduction.txt:1213,搜「Slack」)。 ↩

  23. 出处:「Introduction」(第 5 章)第 1225–1245 段(text/23-fm-introduction.txt:1225,搜「rounding errors」;text/23-fm-introduction.txt:1232,搜「0.005」)。 ↩

  24. 出处:「Introduction」(第 5 章)第 1254–1278 段(text/23-fm-introduction.txt:1254,搜「code, build, test」)。 ↩

  25. 出处:「Introduction」(第 5 章)第 1287–1297 段(text/23-fm-introduction.txt:1294,搜「feature flags」;text/23-fm-introduction.txt:1297,搜「on-call rotations」)。 ↩