3280 字
约 10 分钟
15
S16-A19 · 测试套件的维护账:什么时候该删测试
2024-12-31
2026-11-09

两千条测试涨到六千条,全量跑一次四十分钟。于是本地只跑改动涉及的那三个类,CI 的失败名单挂着一长串红色没人认领,发布前靠一句"上次也是这么发的"过关。这时候套件已经不再是保护措施了,它变成了组织里的另一种存在:写在流程文档里,但没人真正执行。

看清这件事只有一个视角:把测试当成一笔要按年付维护费的资产。维护费不在写它那天付,而在它之后每一次变更里付——改一个方法就要改掉几条用例,换一个接口就要动几个夹具,改一条业务规则就要全局搜一遍散落的期望值。删测试是预算决策,不是态度问题:留下来的每一条都在占用两类稀缺资源,一是每次变更时工程师的注意力,二是每次提交时流水线的墙钟时间。


1. 维护费从哪几个口子流出去

变更类型 触碰的测试面 维护动作 代价信号
改方法签名 所有直接调用它的用例 改调用、改夹具 一次小改动的红测数量
改内部结构 断言中间状态的白盒用例 重写断言 重构时必须同步改测试
改接口或协议 集成用例、契约桩、测试替身 更新桩与录放数据 替身自身的维护量
改业务规则 期望值散落在多处 全局搜期望值并逐条核对 同一条规则被十处重复断言
新增分支 参数化数据、夹具 补数据行 用例数随分支线性增长

关键判断:用例数量线性增长,变更时的维护成本却接近超线性,放大系数不是数量本身,而是"断言与实现的耦合度"。同样六千条用例,断言都压在行为层与断言都压在内部字段层,维护成本差一个量级——前者改实现不用改测试,后者改任何一行都要陪跑。

第二口子是执行时长,它决定套件会不会被真的运行:跑不完的全量等于不存在。它不会报错,只会被绕过;而被绕过一次之后,绕过就变成了默认路径。

// 分层预算是时长分配,不是道德要求
// ─── 提交触发:单元层,预算分钟级(目标是"每次提交都跑")
// ─── 合并门禁:集成 + 契约,预算十分钟级
// ─── 每日 / 发布前:端到端 + 慢用例,预算可达半小时级
// 超预算的用例:要么优化、要么移层、要么删
// 判断依据是分层后的墙钟时间,不是用例条数

2. 测试失效的四种形态

形态 典型表现 危害 处置
永远通过 无断言、断言恒真、被替身架空了真实调用 最危险:提供虚假安全感 补断言,补不出来就删
永远失败 长期红,被排除在门禁之外 训练团队忽略红色,真失败被淹没 限期修或删,禁止长期挂着
随机失败 重跑才过,依赖时序、端口、残留数据 用重跑代替排查,信任流失 修根因或删,不许标记忽略
测了错的东西 断言实现细节、断言替身被调用了几次 锁死重构自由,变更成本翻倍 改写成行为断言或删

四种里最容易被放过、也最贵的是第一种。一条永远通过的测试在报表上是绿灯,在事故现场是干扰项——它占着位置,让"这块有覆盖"的判断落空,而它其实只证明了"这段代码能被调用"。它比"永远失败"更难发现:红色至少有人在看,绿色没人复核。

// 审计被静默的用例:忽略标记是欠账,不是结论
$ grep -rn "@Disabled\|@Ignore\|it.skip\|t.Skip(\|mark.skip" src/test tests \
    | tee skip-audit.txt
// 再找一条断言都没有的用例:这类只证明"代码能被调用"
$ grep -rL "assert" tests/ | tee no-assert.txt
// 统计随机失败:同一用例最近二十次执行的通过率
$ grep -h "FAIL" ci-runs/*.log | awk '{print $3}' | sort | uniq -c | sort -nr | head
// 路径按仓库实际结构核对;注释里出现的 skip 要人工剔除

审计结果通常很难看:忽略标记的数量与团队规模成正比,而每一条忽略标记背后都是一次"先这样,回头再说"。不稳定测试必须修或删,不能标记忽略后就当不存在——忽略等于把它从预算里划掉但保留在账面上,两边的数都不真实。

3. 删除的判据

删测试比写测试更需要判据:写错的成本是维护费,删错的成本是失守。

判据 可验证信号 反例(别删)
覆盖的行为已不存在 被测代码已删除,用例挂在孤立符号上 行为因兼容或外部约定随时可能复活
与其他用例重复 断言集合是另一条的真子集 重复的那条恰是唯一覆盖边界值的
断言的是实现而非行为 断言指向私有方法、内部字段、调用次数 协议与客观约束(字段顺序、错误码)
维护成本高于它能发现的问题 台账里拦住的缺陷为 0,且所在模块改动最频繁 该区域刚出过回归事故
用例其实是一次性验证脚本 人工执行、依赖本地文件、名字带日期 它其实是监控探测或对账作业
依赖的替身已经失真 替身行为与真实依赖长期不一致 真实依赖不可得,替身是唯一手段

判据之后是动作规范,而动作规范更容易被跳过:

// 删除四步,禁止一步到位
// 1) 先降级:移出门禁,仍留在仓库里观察一个完整周期
// 2) 观察期内出现任何一次"本该它拦住"的失败 → 恢复并补强
// 3) 确认安全后删除,提交信息写明依据哪条判据
// 4) 归档交给版本历史,不要留注释掉的大段用例

第 4 条常被违反:注释掉的用例既不被执行也不被清理,比删除更糟——后来者不敢动它,又不能信任它,于是它永久停在那里,每次搜索都会命中一次,消耗一次注意力。

4. 归属、配额与度量

问题 常见做法 判据与代价
谁维护 "谁写谁维护" 写的人离职或转岗后就是无主资产;按"谁会改坏它"定归属,兜底给模块所有者
只补不删 覆盖率只升不降 套件单调膨胀,最终没人跑全量
只删不补 清理行动的产物 关键行为失守,事故反噬比维护费更贵
测试重构的成本 被当成零 夹具抽取、断言改写、数据隔离,量级常与业务改动相当
改写测试的排期 不算工时 低估的后果是"写了但不跑",因为没时间调试它

正确做法是把补与删放进同一本账:每个周期交付三个数——新增 N 条、删除 M 条、修稳 K 条,其中"修稳"指把随机失败改成确定性。三个数长期失衡(M 与 K 恒为 0),说明套件在往不可维护的方向走;而只有第三个数字长期为零,套件其实已经在慢慢腐化,只是还没到痛的那天。

"谁写谁维护"这条原则的失败方式很固定:作者是当时最懂这段逻辑的人,一年后他调走了,用例还在,但没人敢改,因为不知道断言里那个魔数代表什么。更稳的归属是按模块所有者:谁负责这块代码,谁负责让它的测试通过、变快、变准,这样归属随组织图走,而不是随人名走。

// 套件台账:一行一条记录,按周期聚合
// ─── 有效性比例    近期变更中被它拦住的失败占比        长期为 0 → 在陪跑
// ─── 平均执行时长  全量跑完的墙钟时间                  持续上涨且无人削
// ─── 不稳定率      需重跑才通过的用例数 ÷ 总用例数      用重跑掩盖
// ─── 覆盖行为数    与规则条目一一对应的用例数            用行覆盖率替代
// ─── 变更红测数    一次典型改动触发的失败用例数          只涨不降
// ─── 删除速率      每周期删除条数与新增条数之比          恒为 0 → 只进不出

其中覆盖行为数要用来替代行覆盖率:行覆盖率对"这段代码被调用了"敏感,对"这条规则被验证了"麻木。同一个行为在十处被断言,行覆盖率好看,行为覆盖数没变。"测试数量"本身不是质量指标,它常常反向——数字越大,越可能说明断言被摊薄在重复用例里,而不是说明覆盖更完整。

5. 排错:套件过载的六种现场

现象 根因 处理
全量跑不完,没人跑 缺分层预算,个别慢用例吃掉额度 按触发时机分层,先定位最慢的若干条
失败名单长期挂 N 条 没有到期日,红色常态化 每条失败给负责人与到期日,过期强制处理
门禁被绕过合并 红色频繁,绕过成了习惯 门禁只收确定性失败;修稳优先于加新
新人只写实现不写测试 缺可模仿的样板,改测试不计入排期 提供夹具与模板,把"改测试"写进任务估算
删测试后出了事故 删掉的是约定而不是重复 恢复删除四步,降级观察期不可省
行覆盖率涨了事故没少 断言强度低,覆盖的是调用不是规则 用变异测试或故障注入抽样验证断言强度

6. 边界:这些情况不必做套件治理

场景 是否值得 替代动作
测试总数不足百条 收益低 先把关键路径补上,谈不到删
系统即将重写 收益低 只保留行为约定用例,不投资套件重构
用例本质是一次性验证脚本 不适用 归入运维脚本目录,带到期日单独管理
需求本身高频变动 部分适用 保留端到端行为断言,压缩白盒断言
没有 CI 环境 前置缺失 先建最小反馈环,再谈分层与删减
单人项目、短期原型 简化 守住"每次提交自动跑"这一条即可

一句话收束:判断一套测试该不该留,不看它写了多久、覆盖了多少行,而看它在下一次变更里要你付多少钱,以及上一次事故它有没有出声。


自测

  1. 变更是怎样把测试从"资产"变成"负债"的,维护费的放大系数取决于什么?
  2. 四种失效形态里哪一种最危险,为什么它比"永远失败"更难被发现?
  3. "覆盖的行为已不存在"与"行为随时可能复活"之间,判据是什么?
  4. 为什么删除要先降级观察,而不是直接删?
  5. 用"覆盖行为数"替代行覆盖率,避免了哪种误判?
  6. 什么情况下做正式套件治理的收益低于成本?
S16-A19 · 测试套件的维护账:什么时候该删测试
https://www.clxhxhhr.top/posts/6328/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。