QA · 验证台 · 一本关于「怎么知道它错没错」的书
证伪
F A L S I F Y
面试官问「单元测试和集成测试有什么区别」,你能背出标准答案。但真正要命的问题是另一个:AI 给了你一段它「确信」正确的代码,你怎么知道它错没错?你不能再问它一遍 —— 它只会更确信。你也不能靠「读起来对」—— 最坑的 bug 全都读起来对。
这本书的第一句话,来自两个人。Dijkstra:测试只能证明 bug 存在,不能证明 bug 不存在。Popper:你永远无法证明一个东西是对的,只能想办法证明它是错的 —— 证明不了错,我们才「暂时」相信它对。
所以测试不是用来给代码盖「正确」章的。测试是你手里唯一一把,能让 AI 那份完美自信当场露馅的锥子。这本书,教你把「证伪」这件事,做成每天都在跑的工程。
幕 I · 为什么要证伪 WHY FALSIFY
AI 只会给你它「确信」的答案
在写第一个 assert 之前,先想清楚测试到底在对抗什么。这三章不教框架 —— 它们教你为什么在 AI 写大部分代码的年代,「证伪」这件事比以往任何时候都重要。
01
完美的谎言:AI 不知道对错,只知道它以为的对错
一个真的会跑的 Demo:挑一段「AI 写的」代码(均分账单、算页数、求平均、找最大值),先喂它自信的样例 —— 全绿。再让测试去找反例 —— 它当场露馅。average([]) = NaN、1000 分 3 人只发出 999。这个翻转,是全书的主轴。
▸ AI 幻觉复现器
02
一个测试的解剖:Arrange-Act-Assert 与一句断言
一个测试只有三段:准备、执行、断言。而全部重量都压在最后那一句
assert 上 —— 它就是你对「对」的定义。同一次失败,assertTrue 只告诉你「false」,assertEquals 告诉你「期望 80 实际 100」。断言写得好坏,差的是你 debug 的半小时。
▸ 断言解剖台
03
红与绿:先把「对」的定义写下来
TDD 不是宗教,是一种纪律:先写下期望,再让代码去满足它。在 AI 时代这条纪律更值钱 —— 你写测试(把「对」钉死),让 AI 写实现。红→绿→重构,一步一步走给你看,顺带解释为什么「先看到红」本身就是一种验证。
▸ 红绿循环
幕 II · 测试的形状 THE SHAPE
单元、集成、端到端、冒烟 —— 它们各管一段
你听过的那些名字,不是同义词,也不是「谁比谁高级」。它们是一条从快到慢、从小到大、从多到少的谱系,各自回答一个不同的问题。这一幕把它们钉死,并配上 Android 与 Web 的真实代码。
04
测试金字塔:一笔关于成本、速度与信心的账
为什么单元测试要「多而小」、端到端要「少而精」?因为这是一笔账。计算器摆给你看:15 个 e2e 只占 1.6% 的数量,却吃掉 65% 的时间。倒过来(冰淇淋蛋筒)会怎样,拖动滑块当场看到。
▸ 金字塔计算器
05
单元测试:最小、最快、最多的那一层
「单元」到底多大?为什么它该在 JVM 上跑、不碰 Android 框架?FIRST 原则(快、独立、可重复、自验证、及时),JUnit5 + kotlin.test 的真实写法,以及那条最容易被违反的规矩:一个测试只测一件事。
06
集成测试:接缝才是真正出事的地方
单元测试全绿,一上线还是炸 —— 因为 bug 不在方块里,在方块之间的接缝上:Room 的 SQL、Retrofit 的 JSON、时区、编码。用内存版 Room 和 MockWebServer,测那些「单元测试structurally看不见」的裂缝。
07
端到端与 UI 测试:像用户一样点一遍
Espresso、Compose Test、Playwright:让机器像用户一样点按钮、填表单、等结果。它们信心最高,也最慢最脆。为什么 e2e 不能堆成主力(冰淇淋蛋筒反模式),以及 Compose 测试里那个绕不开的「同步」问题。
08
冒烟测试:先确认「它根本没炸」
名字来自硬件:通上电,不冒烟,才继续测。软件里的冒烟测试是一小撮最关键的用例 —— App 能起来吗?登录能进吗?下单能走通吗?它是 CI 流水线的守门员:先花 8 秒确认没白炸,再花 8 分钟跑全量。
▸ 测试选择台
幕 III · 手艺 THE CRAFT
同样一个测试,写好与写坏差了十倍
会调 assertEquals 不等于会测试。这一幕是手艺:怎么挑输入才抓得到 bug、什么时候该用替身、怎么测异步、以及一种让机器替你找反例的写法。
09
挑输入:等价类、边界值,和 bug 藏在哪
你不可能穷举所有输入,但也不用。把输入分成「等价类」,每类挑一个;再重点打边界 —— 因为 bug 几乎都睡在边界上(
> 写成 >=、差一、空、零、溢出)。探测器会让你亲眼看到:内部随手挑测不出的 bug,边界一点就红。
▸ 边界探测器
10
测试替身:dummy / stub / spy / mock / fake
五个总被混着叫的词,其实是一条从「哑」到「有行为」的光谱。哪个只是占位、哪个会回预设答案、哪个会校验交互、哪个是能真跑的仿制品 —— 配 MockK 的真实写法,一次讲清。
▸ 替身光谱
11
别 mock 一切:伦敦学派 vs 底特律学派
mock 上瘾会写出一种测试:它把每个协作者都换成假的,于是测的其实是「我的代码有没有按我以为的方式调用别人」—— 一改重构就全红,却抓不到真 bug。两个学派的分歧、过度 mock 的味道、以及一条中间路线。
12
异步与时间:测协程、Flow,和「等一下」
异步是测试脆弱的头号来源。runTest 的虚拟时钟怎么把「等 5 秒」变成瞬间、Turbine 怎么测 Flow 的每一次发射、为什么 Thread.sleep 是测试里的原罪、以及 Espresso 的 IdlingResource 在解决什么。
13
属性测试:不写样例,写规律
与其手写三个样例,不如写下一条永远成立的规律(排序后一定非降、编码再解码一定还原),让引擎灌一千组随机输入去撞。撞到反例后,收缩引擎会把它砍成人能一眼看懂的最小反例。你甚至不用自己想边界 —— 它替你想。
▸ 收缩引擎
14
快照与契约:把「和上次一样」变成断言
有些东西不好写期望值:一整棵 Compose UI、一大坨 JSON。快照测试把「这次和上次长得一样」变成断言(Roborazzi / Paparazzi 截图对比)。而契约测试解决另一个裂缝:前端和后端各自绿,合起来对不上。附快照测试最容易变成「橡皮图章」的坑。
幕 IV · 谎言与陷阱 LIES & TRAPS
测试自己也会撒谎
最危险的不是没有测试,是有一堆假装在保护你的测试:100% 覆盖率却没有断言、每次结果都不同的 flaky、以及 AI 写的、长得非常专业却什么也没验证的测试。这一幕专门拆穿它们。
15
覆盖率的谎言:100% 覆盖,0 个断言
覆盖率衡量「代码被执行了」,不是「行为被验证了」。Demo 里一套测试拿到 100% 覆盖率、全绿 —— 可 bug 就在被覆盖的那一行上,因为没人问过
total=100 这个边界。覆盖率是有用的反向指标(低说明有洞),但绝不是「测够了」的证明。
▸ 覆盖率 vs 断言
16
变异测试:谁来测试你的测试
覆盖率骗人,那怎么知道测试有没有牙齿?把代码偷偷改坏(>→>=、+→-、删一行),再跑你的测试。测试能让改坏的版本变红 = 杀死变异体(好);照样全绿 = 你的测试有洞。弱测试杀 2/5,强测试杀 5/5,分数当场跳给你看。
▸ 变异体存活
17
那个 97% 通过的测试:抖动从哪来
一个 100 次里过 97 次的测试,比一个稳定失败的测试更坏 —— 它教会整个团队忽略红灯。抖动的四个来源(随机时序、执行顺序、真实时钟、哈希序),为什么「失败重试」是止痛片不是药,以及这个 Demo 本身就是 flaky 的:每点一次,数字都不一样。
▸ 抖动复现器
18
反模式图鉴:十种「测了个寂寞」
测实现细节而非行为(一重构就全红)、断言轮盘(一个测试里十个断言,红了不知哪个)、神秘嘉宾(依赖外部文件)、happy-path 独苗、雪人测试……每一种给出:长什么样、为什么坏、怎么改。code review 里一眼认出它们。
19
当 AI 来写测试:它会作弊,而且很像
全书的收口。让 AI 写测试,它会给你一堆长得极专业、却什么也没验证的东西:照抄实现的重言式、mock 到只剩自己、断言当前(有 bug 的)行为、只覆盖 happy path。怎么审查 AI 写的测试?答案还是这本书:边界、属性、变异分数。你不信任那段代码,就不该信任它自带的测试。
幕 V · 落地 IN PRODUCTION
把它变成每天都在跑的东西
测试写得再好,不跑等于零。最后五章把前面的一切接进真实工程:Android 的测试栈、Web 的镜像栈、CI 什么时候跑什么、怎么让测试跑得快活得久,以及一张能贴进 PR 模板的判断表。
20
Android 测试全景:从 JVM 到真机
一张地图钉死整个栈:
test/ vs androidTest/、Robolectric 为什么能在 JVM 上跑 Android、Hilt 的测试注入、Compose 测试规则、Turbine 测 Flow、Paparazzi 截图。哪些在 JVM(快),哪些必须上真机(慢),以及该往哪层投最多精力。
21
Web 测试全景:Vitest、Testing Library、Playwright
和 Android 对照着看:Vitest/Jest 是单元、Testing Library 是「像用户一样查询」的组件测试、MSW 拦网络、Playwright 是端到端。Testing Library 那句口头禅 ——「测试越像用户用它的方式,给你的信心越足」—— 是整个现代前端测试的哲学。
22
CI 里的测试:什么时候跑什么
测试的价值在于自动、每次、挡得住合并。分层流水线(冒烟秒级→PR 分钟级→夜间全量)、分片并行把 20 分钟压到 3 分钟、必需检查(required checks)、以及 flaky 测试的隔离区(quarantine)怎么设才不会烂尾。
23
让测试跑得快、活得久
测试也是代码,也会腐烂。测试数据构造器、DAMP 而非 DRY(测试里适度重复比过度抽象更好读)、一个失败信息就能定位、别怕删测试。以及速度:哪些慢是必要的,哪些慢是你 sleep 出来的。
24
该测什么、不该测什么:一张判断表
收束。回到第 1 章那段 AI 代码。一张能一路点到底的决策表(纯逻辑?带副作用?UI?AI 生成的遗留?),一份可以贴进 PR 模板的自检清单,以及一个诚实的结论:测试不能证明代码对,但它能让你在 AI 给你惊喜时,第一个知道。
▸ 决策表