The Regression Tests Nobody Writes: Capturing How Software Actually Behaves
TL;DR · AI 摘要
The Regression Tests Nobody Writes: Capturing How Software Actually Behaves - Stack Overflow October 22, 2026 The Regres...
核心要点
- 主题聚焦:The Regression Tests Nobody Writes: Capturing Ho
- 来源:Stack Overflow Blog,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
没人编写的回归测试:捕捉软件的实际行为 - Stack Overflow
2026年10月22日
没人编写的回归测试:捕捉软件的实际行为
[
对于任何运行了几年的代码库,这里有一个令人尴尬的问题。您的测试是描述系统实际的行为,还是描述某人在某个时候认为它应该具备的行为?
这两者并非一回事。通常来说,它们甚至相差甚远。大多数回归测试是在开发功能时由编写代码的人同时编写的,他们记录的是自己的意图。这个函数返回那个值。这个接口会拒绝无效输入。这些都是非常好的测试!但当系统在生产环境中运行几年后,它会积累一层完全未被计划且未被记录的额外行为。一些怪癖。一些奇怪的边界情况。2019年某人出于现在已经完全遗失的原因选择的默认值。我敢打赌,您的某些用户,或者其它团队的服务,可能都静默地依赖着每一个这样的行为。
很多真正棘手的回归问题就隐藏在这里。有趣的是,有一种测试正好可以填补这个空白。几乎没有人编写这种测试。
Hyrum定律与未被记录的行为
您可能听说过Hyrum定律,该定律以Google的Hyrum Wright命名。其核心观点是:一旦足够多的人使用了您的API,系统的所有可观测行为最终都会被某人依赖。不仅仅是文档中定义的契约。所有行为都会被依赖。结果返回的顺序。错误信息的确切措辞。调用所需的时间。一个空字段是显示为null,还是直接不显示。
如果您曾经维护过一个流行的产品,您一定经历过这种情况。您修复了一个明显错误的问题,发布后感到自豪,但不到一小时就会收到一个错误报告,报告指出某人的集成依赖了错误的实现。对他们来说,您刚刚破坏了他们的系统。他们错了吗?其实并没有。
常规的回归测试在这里几乎帮不上忙。它们只检查某人认为需要检查的内容。所有偶然的行为都会悄然溜过。
特征化测试的本质
Michael Feathers在《Working Effectively with Legacy Code》一书中为这种测试方式命名。他称之为特征化测试,这个概念几乎带有一点挑衅意味。在您触碰尚未完全理解的代码之前,先编写测试来捕捉它当前的实际行为。不是它应该具备的行为,而是它实际表现的行为。
运行代码,观察输出结果,并在测试中固定这些结果。看起来像是一个bug?仍然记录下来,或许给自己留个注释。这里的目的是正确性吗?不是。目标是建立一个安全网,一旦行为发生改变就会立即发出警告。
说实话,这需要一种思维方式的转变。常规测试会说:“这就是我想要的,代码,请满足我的要求。”而特征化测试则说:“这就是现实,如果你改变了它,最好有个充分的理由。”
他们也让人感觉不对。编写一个测试来断言你确定是错误的行为,违背了开发者的所有直觉。要真正理解锁定错误才是目的,需要彻底的思维转变。你并不是在说“这样没问题”,而是在确保如果它将来被修改,一定是有人经过深思熟虑才做出的修改。
黄金大师测试与快照测试
多年来人们一直在努力解决这个问题。黄金大师测试(部分人称之为批准测试)会一次性捕获大量输出结果,保存为基准版本,并在之后的每次运行中与它进行比对。前端快照测试基于相同的直觉运行。两者都使特征化测试更加高效,因为它们一次性捕获输出结果,而不是逐个字段进行断言。
它们真正闪耀之处在于可以直接调用的内容。一个函数。一个报告生成器。一个渲染组件。但服务则更复杂。服务的行为不仅仅是返回值,还包括它如何响应真实请求,以及过程中所有操作。它执行了哪些查询。它调用了哪些其他服务。它发送了哪些消息。
通过真实流量表征整个服务
如果你在生产环境中运行服务,这里就变得有趣了。你的服务实际行为的最佳记录并不在任何测试文件中,而是在流量中。
仔细想想。每个真实请求和响应本质上都是尚未编写的一次特征化测试。捕获进入服务的内容和返回的内容,你就锁定了它对真实输入(包括所有无人能想到的奇怪情况)的行为。同时捕获它发送给自身依赖项的内容,你现在也锁定了它的副作用。
在网络层面进行记录使这一切无需修改应用代码即可实现,同时能捕获数据库和队列流量,与HTTP请求同步。之后回放这些记录,使用相同捕获的依赖项响应,你就拥有了一个每次运行行为完全一致的特征化测试套件。相同输入,相同下游行为。如果响应发生变化,或者服务突然多了一次数据库调用,差异分析会立即发现。
最后这一点往往被低估。一些最昂贵的回归问题会保持响应完全不变,却在底层悄悄改变行为。比如一个在测试中无害但在大规模运行时残酷的新查询。基于真实流量构建的测试套件也能捕捉到这些变化。
与特征化测试共处
它们的失败方式与普通测试不同,需要一些时间适应。
一个红色的特征化测试并不意味着“存在错误”,而是意味着“行为发生了变化”。随之而来的第一个问题永远是:我们是否期望这个变化?有时是的。你有意修复了这个特性,警告了依赖它的相关人员,新的行为就成为新的基准。有时不是,恭喜你,你刚刚发现了一个任何基于意图的测试都无法察觉的回归。
无论哪种情况,你都比从用户那里得知要好得多。那些在拉取请求中审查这些差异的团队往往养成了一个非常健康的习惯:行为变化不再是意外,而是决策。差异分析变成了一个小对话:我们真的想要这个变化吗?
但噪音也需要关注。时间戳和生成的ID每次运行都会变化,如果放任不管,它们会淹没真正的信号。忽略这些特定字段,但不要忽略更广泛的字段,测试套件才能保持诚实。
这种方法最能发挥价值的地方
这些情况不断出现,充分证明了这些测试的价值。
重构遗留代码是最典型的场景,这正是Feathers最初所提到的情况。通过固定行为,再在不引发冷汗的情况下调整代码结构。
框架和库的升级是另一个常见场景。你所使用的Web框架或ORM的新重大版本,可能会以一些基于意图的测试从未关注过的方式改变行为,例如序列化方式或结果返回顺序等细微变化。
重构可能是所有场景中最好的案例。当团队将服务迁移到新语言或新架构时,旧服务记录的流量数据基本上就成为了新服务的规范。对两个系统发起相同的请求,任何差异都只能是有意为之的改进或回归问题。再也不需要争论旧系统"应该"如何运作了。
AI辅助的修改场景正变得越来越普遍。当助手起草大规模重构并同时更新测试时,提前捕获的基线数据是为数不多不共享修改假设的检查方式之一。
结论
我逐渐认识到,每个长期维护的代码库实际上有两个规范。一个存在于测试中,另一个存在于实际行为中。大多数回归测试只保护了前者,而对任何有真实用户的系统来说,后者往往才是真正引发问题的那个。
想试试看吗?选择一个你有些害怕修改的服务。在下一次重构之前,先捕获它实际的行为,将修改后的代码与这个基线对比,仔细检查每一个差异。我敢打赌你至少会发现一个自己从未意识到存在的行为。在代码差异中发现它,总比在错误报告中遇到它要舒服得多。
]
这些文章采用知识共享署名-相同方式共享4.0国际许可协议进行授权。
creativecommons.org/licenses/by-sa/4.0/deed.en