没有测试的老系统,先让它自己交代行为

2026-09-24 Aryee 9

改造期最贵的成本不是写新代码,是没人能说清老代码现在到底做了什么。

于是有一个判断我改了很长时间才接受:这套新旧比对的能力要单独占一个批次,而且排在最前面,它不是改造顺手配套的活。 单独排期、单独估工、单独验收。把它当成"改造过程中顺手补的测试",结果是每个后续批次都在没有验收依据的情况下合入。

这一篇只讲证据从哪来、记录记到多细、以及比对出的差异怎么处置。注意与「老系统改造:把重构拆成能验收的批次」那篇里「影子流量的四种形态」区分开——那讲的是流量怎么切,这一篇讲的是证据怎么取,两件事经常被混成一段话。

三条拿证据的路,工程量差一个量级

路线 输入来源 能证到什么 证不到什么 工程量
黄金文件 人工挑定的固定样本 这批输入上行为未变 样本外的形态 低
线上双跑比对 真实流量同时打两侧 真实流量下改前改后一致 有副作用的写路径 中高
录制回放 线上流量的录制件 读路径的覆盖面 幂等、时效性依赖 中

三条不是递进关系,是覆盖面与代价的取舍。只走第一条,结论只覆盖你想到那些情况;只走第二条,写路径根本进不来;只走第三条,会漏掉录制窗口里恰好没出现的形态。

第三条要先破一个幻想:不要预设"有现成的开源回放工具接上就行"。 我核实过的现状是——Java 侧那套最知名的方案(JVM-Sandbox-Repeater)已经实质休眠,release 停在 2022 年、提交停在 2024 年中;Go 侧的 GoReplay 换了仓库但还在维护;更早被引用过的若干 diff 工具仓库直接从 GitHub 消失了。按你自己的语言栈实测之后再决定,多数团队的现实结论是自己写一个只覆盖读接口的采集-回放脚本——它的工作量比把一套半废弃框架适配到当前版本要小,而且你知道它每一步在做什么。

我默认的组合是:第一条建基线并常驻回归,第二条在灰度期跑一段时间,第三条只在纯读接口上做。 全部三条都省、只在预发跑几个用例的那种,不是验证,是自检。

黄金文件:四个决定成败的细节

先分清两个词,它们经常被当同一种东西用:特征测试(characterization test)是"描述它现在做什么、而不是描述它应该做什么"这层意思,出自 Feathers 那本讲遗留代码的书;黄金文件(golden master)是把这个意思自动化落地的一种形态——把当前输出存成基线,之后逐字段比对。这一节讲的是后者的工程细节,做错的点全在其中。

一、输入按等价类枚举,再用真实分布校准。 先手工列形态:不同用户类型 × 不同订单状态 × 边界值 × 异常入参 × 权限分支。列完之后拿一段真实流量的分布回来校准——这一步是为了知道每类形态占多少,避免把样本全花在高频简单场景上。只列不看分布,等价类通常漏两到三类;只看分布不做枚举,长尾形态一条都测不到。

二、快照粒度记到"规范化后的关键字段",不是整个响应。 整响应快照会在两次运行之间产生大量假差异;只记一两个结论字段又掩盖了结构变化。可操作的做法:字段白名单 + 逐字段比对,白名单本身进版本库,改白名单要评审。

三、噪声字段必须先归一化,否则比对结果没有信息量。 常见的五类,逐类都要有处置规则:

  • 时间戳与日期:容差比较,或者剥离出比对集单独看量级
  • traceId / 请求号:直接忽略,但要确认它不影响业务字段
  • 自增主键与序列:忽略值,只比"是否非空、是否唯一"
  • 集合与 map 的顺序:显式排序后再比,除非顺序本身就是契约
  • 浮点与金额:按最小单位取整比对;金额出现浮点差异一律升级为必须修

第四类是陷阱最多的地方。如果顺序是有契约的(前端按下标渲染就是这种情况),排序后比对会主动掩盖真实缺陷。 这一条要在契约清单里单独标注,不能凭默认。

四、基线快照要进版本库,并记录生成时间与来源。 否则半年后没人知道这份"标准答案"哪来的,它就从证据退化成"测试用例"。

黄金文件会锁住 bug,这是它的设计目的

必须说清这一点,不然它会被人当成缺陷而放弃:特征测试记录的是当前行为,包括错误的当前行为。

处置只有一个:把锁定下来的行为显式分成两栏——「要保持」和「顺带要修」。 第二栏不允许静默改动,每一条要走变更确认,且要有业务方点头("这个金额算错了,我们一直这么给客户出账的,你确定要改?"这类对话必须在改造前发生,不是在上线后)。

跳过这一步的团队,会同时得到两个坏结果:要么老 bug 被无声无息地带进新系统,要么被无声无息地改掉,出账对不上时两边都说不清是谁改的。

写接口:比之前先把副作用隔开

读接口好办,写接口的证据难造,因为跑一次就把数据改了一次。三条路,按适用度排:

  1. 隔离存储。 给新实现一套独立库,回放写流量。最干净,工程量也最大——真实数据规模的副本、脱敏、外部依赖打桩,三样都要。
  2. 截断到写之前。 只执行到"即将落库"这一步,把决策结果(要写什么、影响哪几行、走哪个分支)序列化成快照来比对。这条性价比最高,代价是它证明不了落库后的行为。
  3. 副作用打桩并记录调用意图。 外部调用(发消息、调第三方、扣款)全部打桩,比对"调用意图"是否一致。这条要额外确认一件事:桩的清单要从线上真实调用里抓,靠人列一定漏。

三条都不做、直接拿线上写接口做双跑,等价于在生产上做集成测试。这不是激进,是没有回滚。

比对出差异之后:四种去处,不许有第五种

真实比对一定产生差异。每一条差异必须当场给个去处,不能攒着——攒下来的差异列表会在第三次评审时被整体归为"已知问题"。

类别 处置 时限
噪声 加进归一化白名单,写明理由 当场
已知且可接受 记录差异内容、判定人、依据 当场,且必须有非开发方的判定人
还没找到原因 挂工单,指派到人 有到期日
必须修 阻断放量 修完才能继续

规则是:不允许"先不管"这一类存在。 上线那天,「完成」这个词最容易被用坏 里那句"每一条差异都要有交代"就是这个意思:要么有处置结论,要么有判定人和判定依据,两样都没有的,就是没做完。

白名单本身要评审,而且要定期重看。噪声规则一旦写死,两年后它会盖住一个真问题。

抽样量:给算法,不给"跑了十万条"

"我们回放了三万条请求都没问题"这句话没有信息量,因为没人知道那三万里有几种业务形态。

有意义的表达是:某个形态至少出现一次所需的样本量。 若某形态在真实流量里占比 p,希望以置信度 C 至少命中一次,样本量

n ≥ ln(1 − C) / ln(1 − p)

p = 1% 时 n 约是 460(C = 99%);p = 0.1% 时 n 要 4600 以上。这就是为什么"抽十万条"仍然可能一条都没测到关键形态——分布是长尾的,采样量随稀有度是除法关系而不是线性关系。

于是比对报告的正确写法是:按形态报覆盖,不按总量报通过。 每类形态后面跟"跑了多少条、差异多少条、差异怎么处置的"。总量那一行只在估算影响面时有用。

顺序错了,前面全白做

最后一条纪律:同一个批次里不要同时做"补测试"和"换实现"。

正确顺序是三步,中间不能跳:先用特征测试锁住现有行为 → 再在行为不变的前提下拆结构(引适配层、拆依赖)→ 最后才替换实现。

跳步的常见形态是"我边重写边把测试补上"。这样测试实际是按新代码写的,它永远通过,也就永远证明不了什么——而改造验收要的是它能证明不等价的能力。一份从不会失败的测试,比没有测试更贵,因为它占了验收单上一行。

什么算这一层做完了

  1. 黄金文件基线在版本库里,带生成来源,且改白名单需要评审。
  2. 写接口有明确的副作用处置方案,且打桩清单来自线上真实调用。
  3. 差异清单里"还没找到原因"这一档为零,或者每条都有到期日且还没到期。
  4. 覆盖度报表按形态分列,能指出哪些形态样本量为零。

第 4 条最难做到,也最值钱。它给出的不是"通过",而是一份这次改造我们其实没验证到什么的清单——而这份清单才是评审该看的东西。

图示老系统改造中三条拿行为证据的路:黄金文件、线上双跑比对、录制回放

你手上那套系统,卡在哪一步?

留个手机号和方便的时间,我打过来先听你说现状与约束,再给可执行的判断:这套系统是该继续修、该动哪里,还是干脆重做一版设计更省。

约一次沟通不接纯 UI 外包,只做系统层面的问题。