人们谈论软件测试策略时,几乎总是从覆盖率讲起,而覆盖率恰恰是整个领域里信息量最低的一个数字。一个覆盖率达到九十的代码库,照样可能在最常被走到的路径上把缺陷送进生产环境,因为覆盖率衡量的是测试运行期间哪些代码行被执行过,而不是有没有针对这些行做出任何有意义的断言。
真正信任自己测试套件的团队,并不是覆盖率百分比最高的那一批。他们是这样一群人:当东西真的坏了,测试会红;其余时候,测试保持安静。这个性质比大多数人想象的更难用钱买到。
对任何一个测试都值得问的问题: 如果它失败了,我知道该怎么办吗?因为行为改变而失败的测试,会告诉你一些东西。因为某个实现细节挪了位置而失败的测试,只能告诉你有人做了重构。这样的失败积累到一定数量,团队就不再阅读失败信息,而是不断重跑流水线,直到它变绿为止。
为什么覆盖率会误导你
覆盖率回答的是这一行有没有执行过,而不是这一行是否正确,两者完全不是一回事。一个只调用函数、不做任何断言的测试,产生的覆盖率与一个逐一检查结果每条分支的测试完全相同。
这条缝隙会带来实际后果。把目标钉在一个固定百分比上,必然催生出为了凑数字而写的测试:给毫无逻辑的取值方法写穷举用例,而分支难以构造的支付路径上什么都没有。数字上去了,风险纹丝不动。
覆盖率只在一个方向上有用。关键区域覆盖率偏低,是一个值得立刻处理的真实信号。而整体覆盖率高并不能证明任何事情,把它当成目标而不是诊断工具,正是一个团队最后拥有几千个测试却毫无信心的标准路径。
软件测试策略:哪些测试对得起它的成本
每个测试既是资产,也是负债。它需要维护,它拖慢套件,而且它偶尔本身就是错的。有用的问题是:哪些测试能把这份代价赚回来。
单元测试在逻辑确实复杂、并且独立于基础设施时最划算:定价规则、日期处理、权限校验、各类解析器。它们快、精确,而且能在重构中存活,因为它们所描述的行为是真实存在的。
集成测试的回报比多数团队预期的要高,因为生产环境的缺陷大多住在边界上,而不是函数内部。那条对着模拟对象跑得通、对着真实数据库就崩掉的查询。那个可选字段在实际数据里从来不出现的 API。这类测试更慢,但值得。
端到端测试只对少数几条关键旅程划算,数量应当少到可以一口气念出来。注册、下单,以及你的业务真正在做的那一件事。它们慢、脆、贵,一套两百个端到端测试,是一个团队痛苦的主要来源。
大多数代码库最终收敛到的形状是:大量单元测试,一层扎实的集成测试,外加少数几条端到端旅程,也就是 Martin Fowler 描述的测试金字塔 。团队出问题的地方通常在中间那一层:他们有单元测试,也有端到端测试,却几乎没有任何东西在检查各个部件是否真的拼得上。
不稳定测试是信任问题
一个每二十次运行失败一次的测试,比没有这个测试更糟糕,原因与其说是技术性的,不如说是行为性的。
只要套件里混进几个不稳定测试,团队就会学到:红色不一定意味着坏了。重跑变成常规动作。接着,一次真正的失败也被重跑了,等它在第三次尝试时通过,就有人把它合并了进去。此时套件已经不再作为信号发挥作用,却还在继续消耗时间。
请把不稳定当作与生产缺陷同等优先级的问题来处理。先立刻把该测试隔离,让流水线诚实地变绿,然后要么修好它,要么删掉它。常见成因是测试之间共享状态、对真实时间的依赖,以及依赖运行器根本不保证的执行顺序。
删掉一个不稳定测试是完全正当的结局。没有人信任的测试并没有提供任何保护,把它移走至少能停止它继续消耗注意力。
测试行为,而不是实现
测试套件之所以昂贵,最常见的原因是测试被绑在代码怎么做上,而不是代码做了什么上。
把每个依赖都换成模拟对象,然后断言某个特定方法被以特定参数调用过,这样写出来的测试会在任何一次重构后失败,无论行为是否改变。这恰恰是反着来的:重构正是你最希望套件告诉你什么都没坏的时刻,可它却给了你五十条需要逐一手工排查的失败。
替代做法是对结果做断言。给定这样的输入,系统产生这样的输出,或者进入这样的状态。这类测试能在内部实现被完全重写之后存活下来,也就意味着,它们恰恰在风险最高的那些改动中持续保护着你。
模拟对象真正配得上位置的地方是真实边界:支付服务商、邮件服务,以及任何很慢或带有你不能在测试运行中真实触发的副作用的东西。在你自己的代码内部,它们通常的花费高于回报。
让它在 CI 里跑起来
没有人愿意等的套件,就是会被跳过的套件。如果完整运行需要四十分钟,人们会推完代码就走开,而反馈会在他们已经开始做别的事情之后才姗姗来迟。
把它拆开。快速的单元测试与集成测试跑在每一次推送上,几分钟内给出答案。缓慢的端到端旅程放到合并时或按计划执行。这与我们在2026年英国开发团队CI/CD最佳实践 指南中谈到的部署纪律是同一套逻辑。
让失败信息可读。一条只说某个断言为假、却完全不提当时在检查什么的失败,每次都要花掉十分钟的考古工作。用测试所保护的行为来命名测试,失败列表就会直接变成一份关于什么坏掉了的说明。
还要让套件保持确定性。不做真实网络调用,不在不加控制的情况下依赖今天的日期,不对执行顺序做任何假设。每一个非确定性的测试,都是一个尚未发作的不稳定测试。
面对一个没有测试的代码库,从哪里开始
不要试图事后补齐完整覆盖率;投入巨大,而其中大部分保护的是根本没人再改的代码。
从缺陷会造成金钱损失的路径开始,先围绕它们写集成测试,因为按每写一个测试的收益算,这类测试抓到的问题最多。接着,在每一次缺陷修复时补一个测试,并且在动手修之前先把失败复现出来。这样一来,覆盖率会精确地长在缺陷真正发生的地方,而这正是关于风险藏在哪里的最好信号。
Mecanik 在定制软件开发服务 中会评审并搭建测试策略,通常从一个问题开始:哪些失败真的会痛。如果你的套件很大,团队上线时却依然紧张,问题很少出在测试数量上。
延伸阅读: 真正有人读的技术文档 、第一周就能交付的开发者入职 、真正能改变些什么的事故复盘 、API 版本管理:何时打破兼容,如何避免 。
常见问题
高测试覆盖率是一个好目标吗? 单靠它并不是。覆盖率衡量的是测试运行期间哪些代码行被执行过,而不是有没有针对这些行做出任何有意义的断言,所以一个只调用函数、不做断言的测试,得分与一个逐条检查分支的测试完全相同。关键路径上覆盖率偏低是有用的信号;整体百分比很高则证明不了多少东西。
单元测试、集成测试和端到端测试的正确比例是多少? 对确实复杂的逻辑写大量单元测试,配一层分量十足的集成测试,因为生产缺陷大多住在边界上,再加上少数几条你能一口气念出来的端到端旅程。多数团队错在中间层:他们有单元测试和端到端测试,却几乎没有东西在检查各个部件是否拼得上。
我应该如何处理不稳定测试? 把它们当作与生产缺陷同等优先级的问题。先立刻隔离该测试,让流水线保持诚实,然后要么修好它,要么删掉它。只要套件里混进几个不稳定测试,团队就会学到红色不代表坏了,于是条件反射地重跑,最终把一次真正的失败也合并了进去。删掉一个不稳定测试是完全正当的结局。
测试里应该模拟依赖吗? 在真实边界上应该:支付服务商、邮件服务,以及任何很慢或带有副作用的东西。在你自己的代码内部,模拟对象通常花费高于回报,因为断言某个特定方法被以特定参数调用过,会让测试在任何一次重构后失败,无论行为是否改变。
如何给一个完全没有测试的代码库补上测试? 不要试图事后补齐完整覆盖率。围绕缺陷会造成金钱损失的路径写集成测试,因为按每写一个测试的收益算,这类测试抓到的问题最多。接着在每一次缺陷修复时补一个测试,并在动手修之前先把失败复现出来,这样覆盖率就会精确地长在缺陷真正发生的地方。
评论