跳到主要内容

测试网:小步敢迈的前提

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. 可带走的

  1. 测试的价值机制:失败→排查范围=两次全绿之间的改动,几分钟级。
  2. 新写的测试先故意改坏一次,确认它真的会红。
  3. 期望值可以先「随便填→跑→回填真实值」,再用临时错误验证。
  4. 夹具每个测试一份(beforeEach),共享可变夹具是诡异 bug 的温床;const 不锁内容。
  5. 风险驱动:别测纯读写访问函数,把火力给边界条件。
  6. 收到 bug 报告,先写复现测试再修。
  7. 覆盖率是找盲区的工具,不是质量分。
  8. 「测试可能过度」的征兆:改测试比改代码还费时。

9. 原文地图

主题原书章原文位置
自测试的来历第4章 §4.1text/12-ch04.txt:19(搜「类应该包含它们自己的测试代码」)
让计算机检查第4章 §4.1text/12-ch04.txt:26(搜「让计算机来帮我做检查」)
加速的机制第4章 §4.1text/12-ch04.txt:33(搜「前一次运行测试后修改代码引入」)
两条箴言第4章 §4.1text/12-ch04.txt:38(搜「完全自动化」) · text/12-ch04.txt:49(搜「强大的bug侦测器」)
先写测试 / TDD第4章 §4.1text/12-ch04.txt:56(搜「开始动手编码之前」) · text/12-ch04.txt:62(搜「测试驱动开发」)
夹具数据第4章 §4.2text/12-ch04.txt:120(搜「sampleProvinceData」)
第一个测试第4章 §4.3text/12-ch04.txt:214(搜「describe」) · text/12-ch04.txt:217(搜「shortfall」)
故意改坏第4章 §4.3text/12-ch04.txt:242(搜「真的会失败」)
红条绿条第4章 §4.3text/12-ch04.txt:297(搜「绿色条」)
风险驱动第4章 §4.4text/12-ch04.txt:308(搜「风险驱动的行为」)
期望值回填法第4章 §4.4text/12-ch04.txt:331(搜「随便给测试的期望值写了一个数」)
共享夹具 DON'T第4章 §4.4text/12-ch04.txt:343(搜「DON'T DO THIS」)
beforeEach第4章 §4.4text/12-ch04.txt:361(搜「beforeEach」)
修改夹具的测试第4章 §4.5text/12-ch04.txt:397(搜「change production」)
边界条件第4章 §4.6text/12-ch04.txt:474(搜「边界条件」) · text/12-ch04.txt:466(搜「负的需求值」)
failure 与 error 之别第4章 §4.6text/12-ch04.txt:512(搜「测试失败(failure)」)
覆盖率的局限第4章 §4.7text/12-ch04.txt:566(搜「测试覆盖率」)
bug 先写测试第4章 §4.7text/12-ch04.txt:568(搜「单元测试来暴露这个bug」)

Footnotes

  1. 出处:「构筑测试体系」第 19 段(text/12-ch04.txt:19,搜「类应该包含它们自己的测试代码」)与第 26 段(text/12-ch04.txt:26,搜「让计算机来帮我做检查」)。

  2. 出处:「构筑测试体系」第 33 段(text/12-ch04.txt:33,搜「前一次运行测试后修改代码引入」);「从前需要一小时甚至更多时间才能找到的bug,现在最多只要几分钟」在第 36 段(text/12-ch04.txt:36,搜「几分钟就找到了」)。 2

  3. 出处:「构筑测试体系」第 38 段(text/12-ch04.txt:38,搜「完全自动化」)与第 49 段(text/12-ch04.txt:49,搜「强大的bug侦测器」)。

  4. 出处:「构筑测试体系」第 92 段(text/12-ch04.txt:92,搜「与UI分离开」);夹具定义在第 222 段(text/12-ch04.txt:222,搜「测试夹具」)。

  5. 出处:「构筑测试体系」第 120 段(text/12-ch04.txt:120,搜「sampleProvinceData」);数据内容在第 124-129 段(text/12-ch04.txt:124,搜「Byzantium」)。

  6. 出处:「构筑测试体系」第 217 段(text/12-ch04.txt:217,搜「shortfall」)。

  7. 出处:「构筑测试体系」第 242 段(text/12-ch04.txt:242,搜「真的会失败」);失败输出在第 258 段(text/12-ch04.txt:258,搜「AssertionError」)。

  8. 出处:「构筑测试体系」第 331 段(text/12-ch04.txt:331,搜「随便给测试的期望值写了一个数」)。

  9. 出处:「构筑测试体系」第 352 段(text/12-ch04.txt:352,搜「共享测试夹具会使测」)。

  10. 出处:「构筑测试体系」第 361 段(text/12-ch04.txt:361,搜「beforeEach」)。

  11. 出处:「构筑测试体系」第 404 段(text/12-ch04.txt:404,搜「given-when-then」);测试数据在第 397 段(text/12-ch04.txt:397,搜「change production」)。

  12. 出处:「构筑测试体系」第 466 段(text/12-ch04.txt:466,搜「负的需求值」)。

  13. 出处:「构筑测试体系」第 512 段(text/12-ch04.txt:512,搜「测试失败(failure)」);「重构应该保证可观测的行为不发生改变,而类似的错误已经超越可观测的范畴」在第 527 段(text/12-ch04.txt:527,搜「超越可观测的范畴」)。

  14. 出处:「构筑测试体系」第 308 段(text/12-ch04.txt:308,搜「风险驱动的行为」)与第 312 段(text/12-ch04.txt:312,搜「测试不充分」)。

  15. 出处:「构筑测试体系」第 474 段(text/12-ch04.txt:474,搜「边界条件」)。

  16. 出处:「构筑测试体系」第 561 段(text/12-ch04.txt:561,搜「先写一个测试来清楚地复现它」)。

  17. 出处:「构筑测试体系」第 56 段(text/12-ch04.txt:56,搜「开始动手编码之前」)与第 62 段(text/12-ch04.txt:62,搜「测试驱动开发」)。

  18. 出处:「构筑测试体系」第 566 段(text/12-ch04.txt:566,搜「测试覆盖率」)。

  19. 出处:「构筑测试体系」第 571 段(text/12-ch04.txt:571,搜「多大的自信」)。

  20. 出处:「构筑测试体系」第 577 段(text/12-ch04.txt:577,搜「测试就在拖慢我」)。

  21. 出处:「构筑测试体系」第 542 段(text/12-ch04.txt:542,搜「不要因为测试无法捕捉所有的bug」)。

  22. 出处:「构筑测试体系」第 44 段(text/12-ch04.txt:44,搜「1997」)。

  23. 出处:「构筑测试体系」第 71 段(text/12-ch04.txt:71,搜「惊人的收益」)。

  24. 出处:「构筑测试体系」第 376 段(text/12-ch04.txt:376,搜「拖慢测试的运行速度」)。