幕 I · 为什么要证伪CH 03进度 03/24

红与绿:先把「对」的定义写下来

测试驱动开发(TDD)常被讲成一种信仰,搞得像不这么写就是异端。忘掉那些。TDD 的内核只有一句朴素得不能再朴素的话:在写实现之前,先把「我要的东西是什么样」写成一个会失败的测试。然后让代码去满足它。红、绿、重构,三拍一循环。这一章不劝你皈依 —— 它想让你看清一件更实在的事:在 AI 替你写实现的年代,「先写测试」从一种编码风格,变成了你指挥、约束、验收那个不知道对错的机器的方式。

红-绿-重构先写测试先看到红重构的安全网AI 时代的 TDD

三拍:红、绿、重构

TDD 的循环就三步,每一步都短到几分钟:

  1. 红(RED):写一个会失败的测试。它描述你还没实现的一小步行为。跑它 —— 它应该红。
  2. 绿(GREEN):写最少的代码让它变绿。哪怕是硬编码、哪怕丑,只要绿。
  3. 重构(REFACTOR):测试还绿着,现在放心去清理刚才那坨代码。改完再跑,依然绿 —— 你就知道没弄坏。

光说是抽象的。下面这个步进器,拿最经典的 TDD 练习题 FizzBuzz(逢 3 说 Fizz、逢 5 说 Buzz、逢 15 说 FizzBuzz),一步一步走给你看。左边是测试,右边是实现,底下是运行结果。点「下一步」。

走完这七步,你会注意到几个反直觉的地方,它们正是 TDD 的精髓。

为什么要「先看到红」

新手最容易跳过的,就是第一步的「跑一下,确认它红」。「我都知道它会失败,还跑它干嘛?」

因为「先看到红」本身就是一次验证 —— 它验证的是你的测试。

◆ 判词 · 一个从没红过的测试,你不知道它会不会红

如果你直接写完实现再补测试,测试一上来就绿 —— 你怎么知道它是因为代码对了才绿,还是因为它根本测不出错?一个断言写错了对象、一个永远成立的条件、一个忘了 assert 的空测试,都会「绿」。

先看到红,你就确认了两件事:这个测试真的在测东西,而且它测的正是你缺的那个行为。然后你让它变绿,这个「红转绿」的动作,才真正证明了「是这段代码让它过的」。

这直接呼应第 1 章:测试的价值等于它「本可能失败」的程度。一个你亲眼见它失败过的测试,是你唯一能确信「有牙齿」的测试。(第 16 章的变异测试,本质上就是把这件事自动化 —— 故意改坏代码,看测试红不红。)

「最少的代码」为什么可以是假的

Demo 第二步里,让 fizzbuzz(1) 通过的实现是 return n.toString() —— 它对 3、5、15 全是错的。TDD 却说「就这样,别多写」。这看着像偷懒,其实是纪律:

◆ 判词 · 别写测试还没要求的东西

每一行实现代码,都应该是被某个失败的测试逼出来的。没有红色测试要求的功能,就是没有测试保护的功能 —— 也就是随时可能悄悄坏掉、而没人发现的功能。

所以「写最少的代码让它绿」,不是懒,是在保证:你写下的每一点行为,都有一个测试盯着它。想加新行为?先加一个红色测试。

这也顺手解决了一个真实焦虑:「我不可能想全所有情况啊。」对 —— 你不用一开始就想全。TDD 让你一次只面对一个情况,每个情况都留下一个测试。三个月后你回来改这段代码,那些测试就是当年每一个决定的备忘录。

绿色的真正价值:重构时的安全网

Demo 的最后一步(第七步)最能说明问题:实现从一串 if 换成了 buildString,整个内部逻辑都变了,而你一秒就知道没弄坏 —— 因为四个测试仍然全绿。

◆ 判词 · 没有测试,你不敢重构;不敢重构,代码就烂给你看

「重构」是在不改变行为的前提下改进结构。可你怎么确认「行为没变」?肉眼看?那是赌博。一套绿色的测试,就是「行为没变」的自动证明。

这在 AI 时代尤其关键:你让 AI「把这段代码重写得更清晰」,它改完你怎么验收?不是读它的解释(它一定说「功能完全一致」),而是跑那套测试。全绿,才敢信。这是你唯一不受它花言巧语影响的验收手段。

AI 时代的 TDD:你写规格,它写实现

过去有人嫌 TDD 慢:「写测试花的时间,够我把功能写完了。」这个成本账,现在被 AI 彻底改写了。

当写实现这件事几乎免费,「先写一个精确描述期望的测试」的相对成本就骤降 —— 而它的收益不变甚至更高。于是一种非常顺手的工作流出现了:

◈ 落地 · 一种 AI 协作节奏
  1. 你写测试 —— 把「对」的定义、边界、失败路径,用几个测试钉死。这是需要人类判断的部分:业务规则是什么、什么算错。
  2. 让 AI 写实现 —— 把测试(而不只是一句模糊需求)喂给它:「让这些测试通过。」测试是比自然语言精确得多的规格 —— 它不会有歧义,而且能自动判定有没有满足。
  3. 跑测试验收 —— 全绿才算数。红了,把失败信息丢回去让它改。你全程不需要「相信」它,只需要看红绿。

这里的关键转变:测试成了你和 AI 之间的合同。你负责定义正确(意图),它负责实现正确(劳动),测试负责裁决(事实)。三方各归其位。

⚠ 陷阱 · 别让 AI 连测试一起写,然后自己给自己打分

如果你说「给这个函数写实现和测试」,AI 会两个一起给你 —— 而它写的测试,往往正好绕开它自己实现里的 bug(第 2 章、第 19 章)。这等于让考生自己出题自己判卷。

合同的甲方(测试)必须由你把关。可以让 AI 起草测试,但你要亲自补上它漏掉的边界、修掉它「断言当前行为」的偷懒。至少,那个期望值得是你独立算的。

要不要每一行代码都严格 TDD?不必教条。有些探索性的、还没想清楚的代码,先写着试试也没关系。但凡是有明确对错、会被复用、出错代价高的逻辑 —— 尤其是你打算让 AI 来写的 —— 先写测试几乎总是对的。因为那正是你最需要一个「不由谁说了算,由事实说了算」的裁判的地方。

TDD 的三条定律

Bob Martin 把红绿循环压成了三条几乎苛刻的定律。它们听起来极端,但正是这种极端,把「先写测试」从一个口号变成了肌肉记忆:

◆ 判词 · TDD 三定律
  1. 不允许写任何产品代码,除非是为了让一个失败的测试通过
  2. 不允许写超过「刚好导致失败」所需的测试 —— 编译不过,也算失败
  3. 不允许写超过「刚好让当前失败测试通过」所需的产品代码。

翻译成人话:你和代码之间被绑在一个几十秒的循环里,永远是测试先动一小步,代码再追上一小步。你几乎不可能写出一行没有测试盯着的产品代码。这三条你不必字字遵守,但理解它们,你就懂了 TDD 的偏执从何而来:它想保证每一行实现,都有一个当初逼它出生的失败测试。

顺带解释 Demo 里一个容易被忽略的细节:第一步的「红」是一个 NotImplementedError,不是断言失败。连编译不过、抛异常,都算「红」 —— 第二定律说的正是这个。你不需要先把函数写完整再看红,函数体是 TODO() 就够红了。这让循环的每一步都小到可笑,而小,正是 TDD 快的秘密。

用 TDD 修一个线上 bug

红绿循环最实用的一个场景,不是写新功能,是修 bug。假设线上报了个问题:优惠计算在金额正好等于门槛时算错了(第 9 章会看到这是最常见的「差一」)。正确的修法,严格按红绿走:

// 第一步:写一个"复现这个 bug"的测试。它现在应该是红的。
@Test fun `金额正好等于门槛时应打折`() {
    // 报告说:满 100 打八折,但 total 正好 100 时没打折
    assertEquals(80, discount(total = 100))   // 现在实现返回 100 —— 红!
}
discount(100)
expected: <80> but was: <100>
FAIL — 红。我们成功地"抓住"了这个 bug。
◆ 判词 · 先让 bug 变成一个红测试,再去修

这一步(先写红测试)带来三重确认,缺一不可:

  • 确认你真的理解了 bug —— 你能写出一个稳定复现它的测试,说明你搞清了触发条件(是 total == 100 这个边界)。
  • 确认你的修复真的有效 —— 改完实现(把 > 改成 >=),这个测试从红转绿,才证明"是这次改动修好的",而不是你以为修好了。
  • 确认它永不复活 —— 这个测试从此留在套件里(这就是第 8 章的"回归测试")。以后谁不小心把 >= 又改回 >,它立刻红。

反过来,「直接改代码、手动点一下觉得好了」这种修法,三重确认一个都没有。你不确定复现条件、不确定修对了、更不确定它明年会不会又回来。

那段 AI 协作,长什么样

把上面的合同工作流(你写测试、AI 写实现)落成一段具体的对话,你会更有体感:

▲ 一次真实节奏

:先不写实现。这是三个测试(含 total=100 边界、total=0total=50 门槛)。请只写 discount 的实现,让它们全绿。

AI:(给出实现)

:(跑测试)边界那个红了,discount(100) 期望 80 实际 100。修一下。

AI:(把 > 改成 >=)

:(跑测试)全绿。合。

注意整个过程里,你一次都没有去"读它的代码判断对不对",更没有"相信"它的任何解释。你只做了两件事:把"对"定义清楚(测试),看红绿(事实)。这就是 AI 时代 TDD 的全部价值 —— 它把"验证一段你没写的代码"这件难事,变成了"看那盏灯是绿是红"这件易事。

✚ 动手

1. 挑一个小需求(比如「把秒数格式化成 mm:ss」),严格按红-绿-重构走一遍。强迫自己在写实现前先看到红。

2. 试一次「合同」工作流:你写 3-4 个测试(含边界),让 AI 只写实现去满足它们。观察它第一版能过几个 —— 通常会栽在你精心挑的那个边界上。

3. 下次让 AI 重构一段代码时,先确保这段有测试。改完只看红绿,别读它的解释。体会「不受花言巧语影响」是什么感觉。

第一幕结束。你现在知道了测试为什么重要、一个测试长什么样、以及怎么用红绿把它变成一种纪律。第二幕开始,我们给这些测试分类 —— 单元、集成、端到端、冒烟。你会看到,它们不是「谁比谁高级」,而是一条各管一段的谱系。先从那张著名的金字塔说起。