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

完美的谎言:AI 不知道对错,只知道它以为的对错

你让 AI 写一个「求平均分」的函数。三秒后,它给你一段代码,附一句「逻辑很直接,没问题」。它确实没在骗你 —— 在它见过的成千上万段类似代码里,这就是标准答案。它是真心觉得这是对的。问题是:它的「觉得对」里,恰好没有那一行会让它崩溃的输入。而你,正准备把这段代码合进主干。这一章讲的就是这件事 —— 以及为什么在 AI 写下越来越多代码的年代,「想办法证明它错」这件手艺,反而比以前更值钱了。

AI 的确信DijkstraPopper 证伪验证的瓶颈外部真相来源

一段「没问题」的代码

先看这段。它出自一次真实的对话:「帮我写个函数,算一组分数的平均值。」

fun average(xs: List<Int>): Double {
    return xs.sum().toDouble() / xs.size
}

读一遍。它对吗?大概率你会说「对啊,平均值不就是总和除以个数」。AI 也是这么想的,而且它比你自信得多 —— 它甚至懒得停下来问一句「如果列表是空的呢」。

下面这个 Demo 会替你问。你先点「跑 AI 自信的样例」—— 那是 AI 自己会想到的、顺手的输入。全绿。看起来完美无缺。然后点「让测试去证伪」—— 多喂几个它没想到的输入进去。

四段代码,四个经典的「AI 自信 bug」:空列表让平均值变成 NaN;1000 分给 3 个人均分,只发出去 999(一分钱蒸发在四舍五入里);25 条数据每页 10 条,它算出 2 页(最后 5 条用户永远翻不到);一串全是负数的列表,它找出的「最大值」是 0 —— 而 0 根本不在列表里。

这些 bug 有一个共同点,它是整本书的起点:

◆ 判词 · 全绿 ≠ 对

它们全都读起来是对的。它们全都在你顺手想到的输入上是对的。它们只在那几个你(和 AI)都没想到去问的输入上崩溃。

而「顺手想到的输入」和「会出事的输入」,几乎从来不是同一批。bug 就活在这条缝里。

你没法靠「再问一遍」来验证

发现问题后,大多数人的第一反应是:把代码贴回去问 AI「这段有没有 bug?」

试试看。大概率它会回答「这段代码看起来是正确的」,然后热心地给你补几行注释。因为你问的方式,本身就在请它确认,而它最擅长的就是给出一个自洽、流畅、听起来很有道理的确认。它没有一个「跑一遍看看」的动作,除非你逼它。它对「对错」的判断,来自语言模式,不来自执行结果。

▲ AI 的本性

一个语言模型的目标,是输出最可能、最连贯的下一段文本。当那段文本是代码时,它输出的是「看起来最像正确答案的代码」。

「看起来最像正确答案」和「正确」高度相关 —— 这就是它为什么这么有用。但两者不是一回事,而它们分岔的地方,恰恰是那些罕见的、边界的、反直觉的输入。也就是 bug 最爱待的地方。

更麻烦的是:它对自己的输出没有校准过的怀疑。它不会说「这段我七成把握」。它对空列表崩溃的这段代码,和对一段真正健壮的代码,语气一样笃定。它的自信,不携带任何关于正确性的信息。

所以你需要的不是「更聪明的确认」,是一个独立于 AI、独立于你的直觉、能真的把代码跑起来、并且专门去找反例的东西。这个东西,就是测试。

两个死人,一人一句忠告

这不是 AI 时代才有的道理。软件工程史上有两句话,早就把这件事说透了,只是现在它们变得格外扎眼。

🕘 掌故 · Dijkstra,1969

Program testing can be used to show the presence of bugs, but never to show their absence.

测试能证明 bug 存在,永远不能证明 bug 不存在。你跑了一千个用例全绿,只说明「这一千个输入上没问题」;第一千零一个可能就是那颗雷。测试给不了你「它对」的证书 —— 它只能一次次地没能证明它错

听起来很丧?恰恰相反。这句话把测试的目标校准了:你不是在收集「它对」的证据(那是收集不完的),你是在主动出击,想办法让它露馅。测试写得好不好,标准就一个 —— 它有多大本事逼出一个反例

第二句来自科学哲学,比软件还早。

🕘 掌故 · Karl Popper,《猜想与反驳》

Popper 问:科学和伪科学的区别在哪?不在于「能被证据支持」—— 占星术也能找到一堆「灵验」的例子。真正的分界线是可证伪性(falsifiability):一个科学理论必须敢于预言「什么情况下我就是错的」

「所有天鹅都是白的」是科学的,因为一只黑天鹅就能推翻它。看到一万只白天鹅,不能证明它对;但它时刻暴露在被证伪的风险里——这才是它有意义的原因。

把这套搬到代码上,严丝合缝。一段代码的「正确」,你永远证明不了(输入空间太大)。你能做的,是为它写下一堆「如果……它就是错的」的断言 —— 每一个测试,都是一只你放出去找黑天鹅的猎犬。代码活着,不是因为被证明对了,是因为一次又一次地,没能被证明错

这也是这本书叫《证伪》的原因。

「证伪」不是找茬,是一种价值观

这个视角会改变你写测试的姿势,值得说清楚。

一个新手写测试,心里想的是「我要证明我的代码能用」。于是他喂进去的,全是那些他知道会过的输入 —— happy path。测试全绿,他很满足。但这样的测试几乎没有信息量:一个永远不可能失败的测试,和没有测试一样。

◆ 判词 · 一个测试的价值,等于它「本可能失败」的程度

如果一个测试无论代码怎么写都会通过,它什么也没验证(第 15、16 章会用覆盖率和变异测试把这句话量化)。

真正有价值的测试,是那种你写的时候心里有点没底的:边界值、空输入、并发、失败路径、那个「应该不会发生」的情况。它们随时可能红给你看 —— 这正是它们值钱的地方。

所以「证伪」是一种主动的、带点对抗性的心态:写完一个函数,别急着为它高兴,先问自己 ——「我要怎么把它弄崩?」空的、负的、零、超大的、重复的、乱序的、并发的、断网的。你替它想到的每一个刁钻输入,都是你从未来某个凌晨三点的线上事故里,提前偷回来的一觉。

为什么偏偏是现在

测试是门老手艺,但 AI 把它的重要性推到了一个新高度。原因是瓶颈变了

过去,写代码是慢的、贵的。一个功能,人得一行行敲,敲的过程中脑子会不断地过「这里空指针吗」「这个边界对吗」—— 验证是和编写揉在一起的,速度也就被编写的速度限住了。

现在,编写几乎免费了。AI 一分钟能吐出你一小时都写不完的代码,而且它不会在敲的过程中停下来怀疑自己。于是产出海量代码的能力,和判断这些代码对不对的能力,之间裂开了一道巨大的口子。

瓶颈的迁移谁是慢的那一环
从前 编写慢 → 验证顺带做了
现在 编写≈免费 → 验证成了唯一的瓶颈

换句话说:当写代码这件事被外包给了一个不知道对错的机器,「判断对错」就从一项顺带的技能,升格成了你最核心的工作。而判断对错的工程化手段,就是测试。

◆ 判词 · 测试是 AI 缺的那样东西:一个外部真相来源

AI 的「对错观」是内生的、闭环的 —— 它拿自己的模式去评判自己的输出,没有一个来自外部世界的、能反驳它的声音。

测试就是那个声音。它把代码真的跑起来,拿真实的执行结果去撞你写下的期望。它不在乎 AI 多自信、不在乎代码多漂亮 —— 它只报告一件事:这个输入下,它到底对不对。这是整条链路里,唯一一个不由「谁觉得对」说了算,而由「事实」说了算的环节。

这不是说「有了测试就万事大吉」,更不是说「测试是唯一有用的东西」。类型系统、code review、静态分析、可观测性、灰度发布 —— 都在兜底。但测试是这里面最主动、最便宜、最可自动化的一道:它在代码合进主干之前就拦,在每一次改动后自动重跑,而且给出的是二元的、不容狡辩的答案:红,或者绿。

这本书怎么走

知道了「为什么」,剩下的全是「怎么做」。24 章,分五幕:

  • 幕 I · 为什么要证伪(你在这)。一个测试到底由什么构成(第 2 章),以及怎么把「先写下对的定义」变成一种纪律(第 3 章,红绿循环)。
  • 幕 II · 测试的形状。你听过的那些名字 —— 单元、集成、端到端、冒烟 —— 到底各管一段什么,配 Android 和 Web 的真实代码。
  • 幕 III · 手艺。怎么挑输入才抓得到 bug、什么时候用测试替身、怎么测异步、以及让机器替你找反例的属性测试。
  • 幕 IV · 谎言与陷阱。覆盖率的谎言、变异测试、flaky、反模式,以及最要命的一章:当 AI 来替你写测试
  • 幕 V · 落地。Android 测试全景、Web 全景、CI 流水线,和一张能贴进 PR 模板的判断表。

全书以 Android 为主(JUnit5、Robolectric、Espresso、Compose Test、MockK、Turbine),网页为辅(Vitest、Testing Library、Playwright)。语言主要是 Kotlin,少量 TypeScript。每一章能动手的地方,都给你一个真的会跑的 Demo。

✚ 动手

1. 回到上面的 Demo,四个函数都点一遍「样例」再点「证伪」。留意一件事:证伪用的那些反例(空列表、全负数、除不尽),你自己写测试时会不会想到?

2. 找一段你最近让 AI 写的代码(哪怕是个工具函数)。别问 AI「它对吗」,而是问自己:「什么输入能让它崩?」写下三个。有一个能崩,你就赢了。

3. 把这句话记下来,它是全书的锚:「我不是在证明它对,我是在想办法证明它错;证不出来,我才暂时信它。」

下一章,我们把镜头拉近,解剖一个测试的内部构造 —— 你会发现它简单得可笑,只有三段;而全部的重量,都压在最后那一句 assert 上。因为那一句,就是你对「对」的定义。