不写测试不是省事,是把一笔支出挪到后面;真正要算的是挪到后面的利息有多高。
一、六个测试与一次删除
十月十九日,周一,罗清和给灰度开关改最后一句文案。这个开关只服务一个客户的三周试点,十一月上旬下线。他顺手补了六个测试:开、关、非法租户、缺配置、并发两次切换、回滚到默认值。写完四十一分钟,跑一遍 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 项。列的时候不定优先级,只标类别与预估工时;定优先级是另一件事,混在一起容易拖到一项都做不完。
| 类别 | 项数 | 偿还 | 跳过理由 |
|---|---|---|---|
| 计费与订阅 | 4 | 3 | 1 项按比例退款边界,等结账改版一起做 |
| 权限与租户隔离 | 4 | 3 | 1 项依赖多租户数据模型的调整 |
| 迁移与导入 | 3 | 2 | 1 项所在的脚本下季度整体重写 |
| 通知与邮件 | 2 | 1 | 1 项绑在即将更换的第三方模板上 |
| 前端组件 | 3 | 0 | 3 项都指向将被替换的表格组件 |
| 探索性脚本 | 1 | 0 | 1 项只跑过一次,已随脚本删除 |
| 合计 | 17 | 9 | 8 项跳过 |
列表的时候他刻意不写「难度」。难度会让人先挑软的,而测试债的排序依据应该是「守的是哪条路径」:钱、权限、迁移各占一行,剩下的才按工时排。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。
范围声明:本文的工时、触发次数与账本项数均为教学假设,落地时以实际代码生命周期与调用量为准;只讨论是否写测试的判断,不讨论测试框架选型与覆盖率指标。
系列导航
- 回到本组目录:一个人的质量守门:验证预算、回归清单与发布门禁
- 上一篇:不可逆操作:预演、快照、冷却与一个人的双人复核
- 下一篇:质量验收:十项检查、四维评分与一次季度复盘
- 相关:一个人的产品度量:事件字典、指标口径与决策看板
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。