测试成为开发者的日常 — 从单元测试到持续反馈环
测试成为开发者的日常 — 从单元测试到持续反馈环
这一章讲什么: 测试不再是写完代码之后交给 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. 边界与局限
- 所有量化数字(70%、60%、45→10 分钟)均无出处,作为量级感知可以,当基准引用不行。
- 契约测试只给了 Pact 一个名字,没有展开消费者驱动的契约是什么、怎么落地—— 微服务测试里最尖的接缝问题,本章讲得比标题浅。
- 测试金字塔没有给配比数字(单元:集成:E2E 具体几比几),只说「多、中、少」。
- 性能测试被一笔带过(只在质量门里出现 JMeter),压测策略完全留给第 14 章。
- flaky 测试给了对策但没有根因分类(数据竞争?外部依赖?时序假设?),实操时还需别的材料。
8. 可带走的
- 单元测试四原则:隔离(mock 外部)、可重复、毫秒级、清晰如文档;
- AAA 结构:摆输入 → 调函数 → 断言;测试命名说行为,代码评审同样审测试;
- 覆盖率四种,行覆盖会撒谎,分支覆盖拆穿它;目标 70–80%,火力压关键路径;
- 有意义覆盖 > 百分比:删掉冗余 happy path 测试,把省下的力气投进边界与失败路径;
- 集成测接缝(契约),E2E 测旅程;金字塔底座必须大,全靠 E2E 的体系又慢又脆;
- 流水线四层门:提交前 → CI → 预发 → 生产冒烟;高危信号在越左边的门拦住越省钱;
- flaky 测试是信任杀手,宁可修一个flaky,不加十个新测试;
- 反馈环七段循环:监控和事故的教训必须流回设计,循环才算闭。
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」) |
Footnotes
-
出处:「Introduction」(第 5 章)第 61 段(
text/23-fm-introduction.txt:61,搜「smallest testable part」)。 ↩ -
出处:「Introduction」(第 5 章)第 93–155 段(
text/23-fm-introduction.txt:93,搜「calculate_discount」;text/23-fm-introduction.txt:120,搜「assertEqual」)。 ↩ -
出处:「Introduction」(第 5 章)第 168 段(
text/23-fm-introduction.txt:168,搜「arrange, act, assert」)。 ↩ -
出处:「Introduction」(第 5 章)第 168–189 段(
text/23-fm-introduction.txt:174,搜「Mirror the structure」;text/23-fm-introduction.txt:189,搜「edge cases」)。 ↩ -
出处:「Introduction」(第 5 章)第 505 段(
text/23-fm-introduction.txt:505,搜「live database」)。 ↩ -
出处:「Introduction」(第 5 章)第 543–553 段(
text/23-fm-introduction.txt:544,搜「Line coverage」;text/23-fm-introduction.txt:547,搜「Branch coverage」)。 ↩ -
出处:「Introduction」(第 5 章)第 563–590 段(
text/23-fm-introduction.txt:590,搜「B and C branches」)。 ↩ -
出处:「Introduction」(第 5 章)第 600–609 段(
text/23-fm-introduction.txt:600,搜「70 to 80%」)。 ↩ -
出处:「Introduction」(第 5 章)第 629–666 段(
text/23-fm-introduction.txt:663,搜「60% reduction」)。 ↩ -
出处:「Introduction」(第 5 章)第 678–692 段(
text/23-fm-introduction.txt:692,搜「mismatches」)。 ↩ -
出处:「Introduction」(第 5 章)第 718–738 段(
text/23-fm-introduction.txt:721,搜「checkout process」)。 ↩ -
出处:「Introduction」(第 5 章)第 919–928 段(
text/23-fm-introduction.txt:925,搜「transferring funds」)。 ↩ -
出处:「Introduction」(第 5 章)第 781–793 段(
text/23-fm-introduction.txt:785,搜「Flakiness」)。 ↩ -
出处:「Introduction」(第 5 章)第 795–809 段(
text/23-fm-introduction.txt:799,搜「many fast unit tests」)。 ↩ -
出处:「Introduction」(第 5 章)第 702–757 段(
text/23-fm-introduction.txt:702,搜「Pact」;text/23-fm-introduction.txt:748,搜「user-critical flows」)。 ↩ -
出处:「Introduction」(第 5 章)第 851 段(
text/23-fm-introduction.txt:851,搜「everywhere, all the time」)。 ↩ -
出处:「Introduction」(第 5 章)第 970–1073 段(
text/23-fm-introduction.txt:1038,搜「pytest」;text/23-fm-introduction.txt:1056,搜「codecov」)。 ↩ -
出处:「Introduction」(第 5 章)第 1103–1109 段(
text/23-fm-introduction.txt:1103,搜「mocks, retries」;text/23-fm-introduction.txt:1109,搜「ephemeral environments」)。 ↩ -
出处:「Introduction」(第 5 章)第 1119–1156 段(
text/23-fm-introduction.txt:1143,搜「Deployment failures dropped」;text/23-fm-introduction.txt:1146,搜「45 minutes」)。 ↩ -
出处:「Introduction」(第 5 章)第 1165 段(
text/23-fm-introduction.txt:1165,搜「delay in receiving feedback」)。 ↩ -
出处:「Introduction」(第 5 章)第 1171–1182 段(
text/23-fm-introduction.txt:1175,搜「minutes to resolve」)。 ↩ -
出处:「Introduction」(第 5 章)第 1188–1216 段(
text/23-fm-introduction.txt:1203,搜「Trivy」;text/23-fm-introduction.txt:1213,搜「Slack」)。 ↩ -
出处:「Introduction」(第 5 章)第 1225–1245 段(
text/23-fm-introduction.txt:1225,搜「rounding errors」;text/23-fm-introduction.txt:1232,搜「0.005」)。 ↩ -
出处:「Introduction」(第 5 章)第 1254–1278 段(
text/23-fm-introduction.txt:1254,搜「code, build, test」)。 ↩ -
出处:「Introduction」(第 5 章)第 1287–1297 段(
text/23-fm-introduction.txt:1294,搜「feature flags」;text/23-fm-introduction.txt:1297,搜「on-call rotations」)。 ↩