Skip to content

pstack 拆解 09 · 验证(下):单元、行为与数字 ​

高节奏下,你没法在最后一口气验证所有东西,所以验证要能被切成单元、就地检查。

这一篇讲三条:把工作排成可验证的单元,测试只断言可观察的行为,测出来的数字要能解释。

sequence-verifiable-units ​

把工作排成可验证的单元的背后是局部化:在造成破坏的最小单元上抓错最便宜,而在它之后才抓到的 break,定位成本不是线性的,是复利的,因为那时你已经在坏的地基上继续建了。所以它把二分法的原理用在开发节奏上:每完成一个小单元就检查一次,出错就能被锁定在这个单元里。交付层面是同一个道理的另一面:一串有顺序的 commit 本身是一个论证,先失败测试后修复,先基线后改动,先脚手架后功能,读的人不用相信你,看它先红、再看到它绿就够了。

它有一句判据:把工作排成一串小单元,每个单元结束在一个可检查的状态,当前单元绿了再往前走。

它有两个面,执行和交付。

执行。 在 sweep、migration,或者任何一串相似编辑里,每改一个就验证,再开始下一个。每个单元是一个前后括号:已知良好状态,一次改动,跑检查,继续。先 rebase 到干净的 trunk,让每次检查对着真正的基线。当杠杆替你做了编辑,每个单元的检查几乎免费,但仍然要跑,因为免费不是理由,证明才是。

交付。 按能证明工作的顺序堆 commit 和 PR。最典型的形状是失败测试在前,修复在后。别别的故事顺序也成立:重塑之前先做一次减法,治疗之前先捕获基线,功能之前先搭脚手架。每个 commit 单独可落地,整串读起来像一个论证。

test-behavior-not-implementation ​

测行为而不是测实现的背后,是关于测试是什么的定义:测试是行为的规格,不是实现的镜像。一个只断言调用关系的测试,实际上是一台"变更探测器",你一改实现它就叫,但它并不区分"改坏了"和"改好了",于是它花掉 CI 时间和 review 注意力,换来的却不是缺陷探测能力。所以有一个很聪明的检验:如果这段代码 import 的每个函数都返回 undefined,这个测试还过吗;如果还过,它就什么都没观察,不可能因为缺陷而失败。这种测试该重写,或者删掉。

它有一句判据:测试像用户那样调用代码,断言用户观察到的结果,对一个字面的期望值。

原文为什么:常量钉子还会在有人编辑常量或 prompt 时失败,于是它实际拦住的,是那次编辑。

有五种测试会在此检验下现形,它们在被 import 的函数全返回 undefined 时仍然通过。

  • 弱断言,或者没有断言。
  • 只测 mock,或者只测"没发生"。
  • 自指,期望值来自被测代码本身。
  • 常量钉子,断言复述一个手工维护的常量或 prompt 字符串。
  • fixture 断言 fixture,断言读的是测试自己造出来的数据。

修法是:用具体输入在测试体里调用被测对象,断言字面输出或可观察效果。要断言"没有",就在同一个测试里对另一个输入断言"有"。要测一个常量,就测读它的那个机制,用一个输入,而不是复述那个值。要测一个 mock,就断言它收到的载荷或调用之后的状态,而不是断言它被调用了。

有两类测试要保留:一张表的行和另一张表的行之间的关系测试,以及 *.test-d.ts 里的编译期检查。

explain-the-number ​

解释这个数字的背后是测量的有效性:一个数字是一个关于系统的声明,而声明可以是真的,也可以只是看起来是真的。一次跑坏了的运行照样会打印一个合理的数字,因为失败的请求、被缓存跳过的工作、从没真正跑过的代码、没调优的一侧、以及 run-to-run 的噪声,都会产生看起来没问题的结果。所以拿到数字的第一件事不是用它,是先问它到底测了什么。这里最有用的一问是"为什么不是翻倍":如果你说不出是什么限制了这个结果,你还没有理解你测的东西。

它有一句判据:在你信任、报告或据以一个数字行动之前,先找出什么限制了它,并排除它测的是别的东西。

它的做法。

问"为什么不是翻倍?" 说出那个限制结果的资源或代码路径:一个核、一把锁、磁盘、网络,或者 load generator 自己。从 profile 或者运行期间的系统计数器得到它,再映射回源码。从读代码猜出来的不算限制。

列出这个数字还可能测的是别的什么,每一项用证据排除。 常见嫌疑是错误、跳过或被缓存的工作、没有调优的那一边、噪声、以及端到端里太小的那一块。

把证据和数字放在一起。 run count、spread、limiter。性能数字就去走 benchmark-checklist 的整套流程,它把这条原则变成了七个要答案的问题。

参考 ​

原文都在 pstack 上游仓库(cursor/plugins):