验证的边际收益:什么时候可以不写测试

灰度开关下周就删,他仍给六个状态各写了一个测试,删除时一起删掉。三种情形可以不写、三种必须写,用十二分钟与四十次触发算出单测净省五点一三小时;十七项测试债季度结算时只偿还九项六点五小时,跳过八项各写了触发条件,留下一张可对账的账本。

不写测试不是省事,是把一笔支出挪到后面;真正要算的是挪到后面的利息有多高。

一、六个测试与一次删除

十月十九日,周一,罗清和给灰度开关改最后一句文案。这个开关只服务一个客户的三周试点,十一月上旬下线。他顺手补了六个测试:开、关、非法租户、缺配置、并发两次切换、回滚到默认值。写完四十一分钟,跑一遍 1.8 秒,之后每周跑两遍。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

三周后开关下线,六个测试在同一个提交里被一起删掉,净贡献是零。他不是第一次这样:前一年还给一次性补数脚本写过 CLI 参数测试,给即将替换的表格组件写过快照测试。这些东西的共同点是生命周期比测试本身短。

如果换一种做法,手动点这六种状态需要四分钟:开一次、关一次、换个租户试一次、把配置删掉试一次、连点两次、把值改回默认。四分钟与四十一分钟之间差的不是纪律,是判断。他当时把「测试覆盖率」当成了目标,而不是把「这一段值得守多久」当成目标。

测试的收益不取决于它写得多好,取决于它会被执行多少次。 一个只活三周、每周跑两遍的测试累计执行六次;一个守着计费状态机的测试一年被执行上千次。同样四十一分钟,投在两处的回报差两个数量级。

所以真问题不是「该不该写测试」,而是「哪一类代码的测试回报低于成本」。判据可以压成三个变量:还要活多久、坏了多贵、被改多频。三个都低,测试就是纯支出;任何一个高,就得重新算。

下面三节按顺序拆:可以先不写的三种情形、必须写的三种情形、一条回本公式与十七项测试债的结算。

二、三种可以先不写的情形

先给判据再给例子。三种主要形态是脚本、探索期界面与将删代码;后面三种是同源的低回报情形;最后一行是常见误判。前六行的期望执行次数都不足以覆盖写测试的时间。

情形判据例子决定
一次性脚本只执行一次,不进产品路径给 1,240 行 CSV 补一列负责人不写,跑完即删
探索期 UI两周内会整体推倒新看板的三种布局方案不写,留截图与一句结论
将删代码已有下线日期与删除提交三周试点的灰度开关不写,手动点六种状态
胶水转换无分支、少于五行、错了立刻看见日期格式化、单位换算不写
文案与常量值错了一眼可见、无分支空状态文案、帮助中心链接不写
临时报表查询只读、跑一次、结果人工看上季度活跃租户统计不写
误判:像胶水有分支、有默认值、有副作用「默认值取租户所在时区」写,两个用例

一次性脚本的判据是「不进产品路径」。补数脚本在本地跑、结果人工核对、跑完删掉,它的失败代价是一次重跑,不是一次事故。给这种脚本写参数校验测试,相当于给一把扳手配说明书。

探索期 UI 的判据是「两周内会推倒」。这时候真正的风险是方向错,不是边界条件错;六个测试保不住一个该被推翻的布局。有效的验证是截一张图、发给三个用户、记一句「第二种布局没人选」。

将删代码的判据是「已有删除提交」。灰度开关这类代码的价值在删得快,不在守得牢;写测试反而会给删除加一道心理成本,因为删的时候要连带删测试,看起来像在破坏什么。

最后一行是边界。判断胶水不能只看行数,要看有没有分支与副作用。「默认值取租户所在时区」只有三行,但它有分支(时区缺失怎么办)、有外部依赖(时区库)、有默认值(错了会让所有新任务落到 UTC);这三样凑齐,两个用例就该写。

表格里的「决定」一列只有两种取值,这一点很重要。允许出现第三种「先不写,回头补」时,所有条目都会滑向第三种,因为它是当下最省力又最不内疚的选项。要么写进测试债账本并给它触发条件,要么明确写「不写」,不留中间态。

还有一个常被忽略的省法:把测试写在更便宜的位置。同一个验证,写在端到端里要 42 秒,写在单元里要 0.03 秒;如果它本来就能在单元层验证,把它放到端到端只是花钱买安心,不是提高质量。

共同前提是:不写测试的代码必须不进产品主路径。一旦被别处 import,它就从脚本变成了依赖,判据立刻翻转。

三、三种必须写:钱、权限、迁移

反过来只有三类没有讨论余地:钱、权限、迁移。它们的共同点是不可逆或难察觉——错了不会红,只会静默算错。

钱的例子是计费状态机。试用转付费、按比例升级、退款、失败重试,四个动作两两相邻,任何一个状态跳错都会开出错误的发票。这类代码一年被改十几次,每次改动都可能碰边界,测试是唯一能在发布前发现「升级时多算了一天」的手段。

权限的例子是租户隔离。一个查询忘了带 workspace_id,界面上一切正常,只是把别人的任务列表也查了出来;这种缺陷不抛异常,也不写日志。判据很硬:凡是拼接查询条件的地方,都要有一个「用另一个租户的身份请求」的用例。

迁移的例子是数据结构变更。加列、回填、切读、删旧列,四步里任何一步重跑都可能写坏数据。这类测试不必覆盖全部行,但要覆盖三种边界:空值、超大值、重复键。

三类之外还有一类边缘情况值得单说:通知与邮件。它不影响钱,也不涉及权限,但发错了客户会立刻看见——把试用提醒发给已付费客户,或者把内部备注写进邮件正文。判据不是「重要」,而是「错误可被外部看见」,可被看见就该有一个断言。

写不写还可以用一个更简单的公式兜底:净收益 = 年触发次数 × 单次省下的时间 − 写测试的时间。写成 12 分钟、一年触发 40 次、每次省 8 分钟,年省 320 分钟,扣掉 12 分钟仍有 5.13 小时的净收益。若一年只触发 3 次,同一个测试的净收益只剩 12 分钟,接近零。

公式的用处不是精确,是把「感觉值得」变成「算出来不值得」。当触发次数估不出来时,说明这段代码的调用路径还没人看清,那本身就是该先写测试的信号。

公式还有一个反向用法:算出来净收益极高时,说明这段测试不该只写一个,而该写三个——因为它的失败代价被低估了。钱与权限两类通常落在这个区间,一次线上算错账的修复成本是 100 倍量级(按本组统一的教学假设),不是 8 分钟能比的。

公式里的「省下的时间」要按真实损失填,不要按理想值填。测试拦住一次缺陷,省下的是发现、定位、修复、沟通四段之和;对一个 P1 缺陷,这个数字是 4 小时而不是 8 分钟。用 8 分钟算,是为了保守:只统计「本地改完立刻跑一遍就发现」的那一类。

四、十七项测试债与一次结算

年中他把所有「知道该测但没测」的地方列成一张表,一共 17 项。列的时候不定优先级,只标类别与预估工时;定优先级是另一件事,混在一起容易拖到一项都做不完。

类别项数偿还跳过理由
计费与订阅431 项按比例退款边界,等结账改版一起做
权限与租户隔离431 项依赖多租户数据模型的调整
迁移与导入321 项所在的脚本下季度整体重写
通知与邮件211 项绑在即将更换的第三方模板上
前端组件303 项都指向将被替换的表格组件
探索性脚本101 项只跑过一次,已随脚本删除
合计1798 项跳过

列表的时候他刻意不写「难度」。难度会让人先挑软的,而测试债的排序依据应该是「守的是哪条路径」:钱、权限、迁移各占一行,剩下的才按工时排。17 项里有 8 项守的是计费与权限,这 8 项里至少 6 项必须在当季处理完。

偿还的 9 项合计 6.5 小时,平均每项 43 分钟。这个数字比预估低,原因是九项里有六项只是补一个已有场景的断言,改的是测试文件而不是产品代码;真正需要先重构才好测的只有三项。

账本还有一列没写进表格:这一项上次被改动的时间。一个月内改过三次的地方,测试债的利息最高,因为它随时可能再被碰一次;两年没动过的地方,即使欠着也排后面。这一列不需要精确,写「上周」「去年」就够用。

跳过的 8 项不是「以后再还」,而是「明确不还」。每一条都写了下一次检查的时间点或触发条件:模型调整完、结账改版上线、脚本重写完成。没有触发条件的跳过项会被重新放回账本,否则「以后」等于「永不」。

复算:
单测:写 12 分钟 = 0.2 h
年触发 40 次 × 每次省 8 分钟 = 320 分钟 = 5.333 h
净省 5.333 − 0.2 = 5.133 h ≈ 5.13 h
测试债:偿还 9 项合计 6.5 h = 390 分钟
390 ÷ 9 = 43.33 分钟/项
偿还率 9 ÷ 17 = 52.9%,跳过 8 项

结算是按季度做的,一年四次,每次不超过两小时:把新欠的项加进去,把已还的项划掉,把过期的跳过项重新排期。这个频率刚好能挡住账本变成日记,又不至于每周都要动它。

边界在这里:回本公式算的是时间,不是风险。三项跳过的测试如果恰好守着一条不可逆路径,省下的 43 分钟可能换来一次三小时的恢复。所以公式只用于排序,不用于最终决定;不可逆路径上的项一律不参与「跳过」这一类。

五、行动清单与复算验收

把判据、公式与账本收进同一张表,再用八条检查确认它没有退化成「凭感觉不写」。

  • 给每段新代码标一个生命周期:一次性、探索期、有删除日期、长期
  • 生命周期短于测试维护期的,明确写下「不写」与手动验证方式
  • 钱、权限、迁移三类代码,无论生命周期多短都至少一个用例
  • 按「年触发次数 × 单次省时 − 写的时间」算净收益,为负就写进账本
  • 测试债账本记四列:类别、项数、偿还、跳过理由
  • 每条跳过项写下触发条件,没有触发条件的重新放回账本
  • 每季度结算一次,偿还率低于五成时先看跳过理由是否仍然成立
  • 不可逆路径上的债不参与跳过,只能排「何时还」

验收标准:任意一段新代码都能回答「它活多久、坏了多贵、被改多频」,并且测试债账本里没有一条跳过项缺少触发条件。

使用边界与实操验收

边界一:不写测试不等于不验证。 一次性脚本仍要人工核对结果,探索期 UI 仍要给至少三个用户看,将删代码仍要手动点一遍主流程;省掉的是自动化,不是验证动作。

边界二:回本公式假设单次省时稳定。 若一次测试失败需要半小时定位,8 分钟的均值就被高估;把定位时间计入单次成本,公式才有意义。样本少于十次触发时,不要用公式下结论。

边界三:测试债账本不是待办清单。 它记录的是「明确决定不还」与「明确决定何时还」,两者都要有理由;把账本当成 backlog 会让它无限增长,最后没人再看。

边界四:本文只讨论单人产品的测试取舍。 多人团队的分工、评审与覆盖率门槛不在范围内;本文不构成对任何具体项目测试策略或交付质量的承诺。

关键参考与核验范围

  • The Practical Test Pyramid :用于核对分层测试的成本差异,以及为什么高层测试不应承担全部验证。
  • Vitest Guide :用于核对单测运行器的基本用法与单次运行耗时的口径。
  • CanaryRelease :用于核对小范围试点与灰度下线的验证方式。
  • GitHub Actions 文档 :用于核对流水线触发次数与执行记录的留存方式。
  • Node.js 测试运行器 :用于核对无额外框架时写一个断言的最小成本,验证「12 分钟」量级是否成立。

以上链接于 2026-10-26 实测可公开访问;未核验版本、价格与配额。核验日期:2026-10-26。

范围声明:本文的工时、触发次数与账本项数均为教学假设,落地时以实际代码生命周期与调用量为准;只讨论是否写测试的判断,不讨论测试框架选型与覆盖率指标。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元