测试网:小步敢迈的前提
1. 这一章讲什么
第 1 章的每一步手术都靠一句「跑测试,绿了,提交」撑着。这一章讲这张网的来历、搭法和用法。原书立场很硬:重构的前提就是一套可靠的测试;但写好测试的价值不依赖重构单独成立——这是全章最反直觉、也是作者最着力论证的一点。
2. 顶层全景:测试为什么能加速
程序员的多数时间不在写代码,在调试;而修复 bug 快,找到 bug 慢。作者 1992 年从一个会上听到「类应该包含它们自己的测试代码」,从此把测试和代码放在一起,并且让计算机代替人检查输出:期望值写进测试,全部通过屏幕只打一个 OK1。
写一点功能 → 立刻补测试 → 跑(几秒)
│ 绿 → 继续
└ 红 → bug 必在「上次全绿之后改的那一小段」里
→ 排查范围:几行,而不是几小时
图说:加速的机制不在「测试找得多全」,在「失败时锁定排查范围」。频繁运行 + 范围极小,找 bug 从一小时级降到几分钟级——这是作者自己的经验叙述2。
由此推出两条纪律,原书都以箴言形式钉死:测试必须「完全自动化,让它们检查自己的测试结果」;以及「一套测试就是一个强大的bug侦测器」——它的价值是缩小查找范围,不是证明正确3。
3. 主走查:给一个生产计划类织网
原书的被测对象是一个生产计划系统:每个行省(province)有需求量、采购价格和一串生产商,程序算缺额(需求减总产量)和总利润。业务逻辑只有两个类,不碰界面——这也是第一条工程判断:业务逻辑一复杂就和 UI 分开,才能便宜地测试4。
第一步:固定夹具。 测试用一个函数返回标准数据:行省 Asia,三个生产商 Byzantium(成本 10、产量 9)、Attalia(12、10)、Sinope(10、6),需求 30,价格 205。这份「测试所需要的数据和对象」叫测试夹具(fixture)。
第二步:第一个测试。 断言缺额:expect(asia.shortfall).equal(5)。手算核对:总产量 9+10+6=25,缺额 30−25=5——这个 5 是原书给出的期望值,不是编的。跑一次,1 passing6。
第三步:故意改坏。 作者对「全绿」天然怀疑:万一测试根本没在检查呢?于是暂时把计算改成 总产量 × 2 再跑——0 passing, 1 failing,报出「expected -20 to equal 5」7。箴言:总是确保测试不该通过时真的会失败。
第四步:第二个测试与期望值的来历。 再测利润 230。这个数怎么来的?作者坦白:先随便写一个数,跑一遍,把程序吐出的真实值(230)填回去——现在的代码是能跑的,暂时相信它;然后再插一次假逻辑确认测试会红,最后恢复8。(利润 230 也能按原书类代码复核:已满足需求 = min(30, 25) = 25 件,需求价值 25×20 售价 = 500;成本按造价从低到高填——Byzantium 9 件×10 + Sinope 6 件×10 + Attalia 10 件×12 = 270;利润 500−270 = 230。)
第五步:夹具隔离。 两个测试都要新建行省对象。把对象提到外层共享?作者在代码里直接写注释 DON'T DO THIS:JavaScript 的 const 只锁引用不锁内容,一个测试改了共享对象,测试就互相污染,而且失败规律诡异到动摇你对整个测试体系的信心9。正解是 beforeEach:每个测试前重建一份全新夹具,测试彼此独立10。
第六步:测修改行为。 改产量:asia.producers[0].production = "20"(界面传来的其实是字符串,设值函数里解析(把字符串转成数值)),然后验证缺额变 −6、利润变 292——两个断言关系紧密,放同一个测试里可以接受。这个「配置→操作→验证」的三段式,各种测试文献叫法不同(given-when-then 等),结构是同一个11。
第七步:探测边界。 空生产商集合:缺额 30、利润 0;需求 0:缺额 −25、利润 0;需求 −1:缺额 −26、利润 −10——写到这作者停了一下:负需求算出负利润,对这个业务有意义吗?写测试逼出了对需求本身的追问12。空字符串需求:结果 NaN。还有一条妙笔:传 producers: "" 进构造函数,抛出的 TypeError: doc.producers.forEach is not a function 在 Mocha 里算「失败」,多数框架会算「错误」——这类异常超出「可观察行为」的范畴,重构不保证保持它,对应的测试甚至可以在重构前删掉13。
4. 怎么写:几条操作规程
- 风险驱动,不追求覆盖。「测试所有 public 函数」是错的目标;只读写一个字段的访问函数不值得测。写太多测试反而导致测试不充分——精力被摊薄了14。
- 边界火力集中。「考虑可能出错的边界条件,把测试火力集中在那儿」:集合为空、0、负值、空字符串15。
- 测试也是代码:迭代(写了改、改了再写)式地写、重构、回顾;每遇一个 bug,先写一个能复现它的测试,测试通过才算修完——「只要测试存在一天,我就知道这个错误永远不会再复现」16。
- 先写测试是常态:写测试就是在问「为了加这个功能我需要实现些什么」,心思被迫放在接口上。Kent Beck 把这个习惯提炼成测试驱动开发(TDD):测试、编码、重构的循环,每小时来好几轮17。
5. 测多少才算够
没有好指标。测试覆盖率(被测试执行到的代码占比)只能指出没覆盖到的地方,衡量不了质量;盲目追求覆盖率会适得其反18。作者给的标准是主观但诚实:「如果有人在代码里引入了一个缺陷,你有多大的自信它能被测试集揪出来?」——重构完看到全绿敢直接继续,就够了19。上限也有征兆:改测试花的时间比改代码还多,说明测试过度了;但它远比测试不足少见20。
6. 作者的判断与证据
| 判断 | 证据 |
|---|---|
| 写测试提高编程速度,即使不重构 | 作者自述:1992 年起个人实践,效率「大大提高」;机制=失败范围小2 |
| 「测试不能证明没有 bug」不妨碍写测试 | 逻辑论证:测试的目的是提高信心与速度,不是证明;「不要因为测试无法捕捉所有的bug就不写测试,因为测试的确可以捕捉到大多数bug」21 |
| JUnit 的诞生 | 1997 年 Kent Beck 飞机上与 Erich Gamma 结对移植(史实叙述)22 |
判断(我们的,不是书里的): 这一章和第 02 章合起来,给出了整本书真正的公理系统——「行为保持」定义了重构,「测试网」定义了行为保持可被观察。第 02 章末尾的判断在这里兑现:没有测试网,小步只是姿势;有了它,小步才真正安全。 如果错,会错在: 如果自动化工具能对某类改动提供数学级别的安全保证(原书 2.10 承认部分工具重构可以不跑测试),这部分代码可以脱离测试网,但那是少数派路线。
7. 边界与局限
- 原书明说这不是测试专章:框架选型(Mocha/Chai)、mock、集成测试都点到为止;「少许测试往往就足以带来惊人的收益」23。
- 「每次重建夹具会不会慢」作者承认大多数时候无感,真成了瓶颈才考虑共享不可变夹具24。
- 遗留代码没测试怎么办,本章不解决(接缝技术放在第 12 章讲)。
8. 可带走的
- 测试的价值机制:失败→排查范围=两次全绿之间的改动,几分钟级。
- 新写的测试先故意改坏一次,确认它真的会红。
- 期望值可以先「随便填→跑→回填真实值」,再用临时错误验证。
- 夹具每个测试一份(beforeEach),共享可变夹具是诡异 bug 的温床;const 不锁内容。
- 风险驱动:别测纯读写访问函数,把火力给边界条件。
- 收到 bug 报告,先写复现测试再修。
- 覆盖率是找盲区的工具,不是质量分。
- 「测试可能过度」的征兆:改测试比改代码还费时。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 自测试的来历 | 第4章 §4.1 | text/12-ch04.txt:19(搜「类应该包含它们自己的测试代码」) |
| 让计算机检查 | 第4章 §4.1 | text/12-ch04.txt:26(搜「让计算机来帮我做检查」) |
| 加速的机制 | 第4章 §4.1 | text/12-ch04.txt:33(搜「前一次运行测试后修改代码引入」) |
| 两条箴言 | 第4章 §4.1 | text/12-ch04.txt:38(搜「完全自动化」) · text/12-ch04.txt:49(搜「强大的bug侦测器」) |
| 先写测试 / TDD | 第4章 §4.1 | text/12-ch04.txt:56(搜「开始动手编码之前」) · text/12-ch04.txt:62(搜「测试驱动开发」) |
| 夹具数据 | 第4章 §4.2 | text/12-ch04.txt:120(搜「sampleProvinceData」) |
| 第一个测试 | 第4章 §4.3 | text/12-ch04.txt:214(搜「describe」) · text/12-ch04.txt:217(搜「shortfall」) |
| 故意改坏 | 第4章 §4.3 | text/12-ch04.txt:242(搜「真的会失败」) |
| 红条绿条 | 第4章 §4.3 | text/12-ch04.txt:297(搜「绿色条」) |
| 风险驱动 | 第4章 §4.4 | text/12-ch04.txt:308(搜「风险驱动的行为」) |
| 期望值回填法 | 第4章 §4.4 | text/12-ch04.txt:331(搜「随便给测试的期望值写了一个数」) |
| 共享夹具 DON'T | 第4章 §4.4 | text/12-ch04.txt:343(搜「DON'T DO THIS」) |
| beforeEach | 第4章 §4.4 | text/12-ch04.txt:361(搜「beforeEach」) |
| 修改夹具的测试 | 第4章 §4.5 | text/12-ch04.txt:397(搜「change production」) |
| 边界条件 | 第4章 §4.6 | text/12-ch04.txt:474(搜「边界条件」) · text/12-ch04.txt:466(搜「负的需求值」) |
| failure 与 error 之别 | 第4章 §4.6 | text/12-ch04.txt:512(搜「测试失败(failure)」) |
| 覆盖率的局限 | 第4章 §4.7 | text/12-ch04.txt:566(搜「测试覆盖率」) |
| bug 先写测试 | 第4章 §4.7 | text/12-ch04.txt:568(搜「单元测试来暴露这个bug」) |