2.4

View in English

2.4 测试策略

概述与动机

测试策略是一系列经过深思熟虑的选择:测试什么、在哪个层级测试、自动化到什么程度、要达到多高的信心水平,从而使你的团队能够快速修改代码而不破坏它。测试是让大型组织能够频繁、安全地部署的原因。它们把预期行为编码下来,捕获回归问题,并让工程师有信心进行重构。如果没有一套连贯的策略,测试往往会走向两种糟糕的方向之一:要么缺失(由恐惧驱动、进展缓慢的开发方式),要么臃肿(成千上万条缓慢、不稳定、没有人信任的测试)。

对一个大型团队而言,策略比任何单一测试都更重要。数百名在同一个代码库中工作的工程师,需要一张快速、可靠的安全网。没有它,每一次改动都充满风险,每一次发布都会变成一场手动的煎熬。测试同时也是意图行为的可执行文档,一旦最初的作者离开,这份文档就变得无比珍贵。这套策略决定了你的测试套件究竟是一项能加快交付的资产,还是一项拖慢交付的负债。

在企业和政府场景中,测试承载着额外的分量。法规可能要求提供有据可查的测试覆盖率和证据。安全关键型和面向公民的系统需要高度的保证。无障碍性和安全测试可能是法律要求的。因此,这套策略必须在速度、信心、成本和合规之间取得平衡,并且必须把覆盖率当作一个信号,而不是一个可以被操纵应付的目标。

关键原则

  • 测试是为了获得改动的信心,而不是为了达到某个数字。
  • 优先选择快速、可靠、相互隔离的测试。缓慢或不稳定的测试会侵蚀使一个测试套件有用所依赖的信任。
  • 把测试推到能够提供真正信心的最低层级,把缓慢、宽泛的测试留给真正存在的集成风险。
  • 一个不稳定的测试就是一个坏掉的测试。要把不稳定性当作一等缺陷来对待。
  • 覆盖率是一个信号,不是一个目标。对琐碎代码的高覆盖率说明不了什么。
  • 测试行为和契约,而不是实现细节,这样你的测试才能在重构中存活下来。
  • 让非功能性测试(无障碍性、性能、安全)成为策略的一部分,而不是事后才想起的补充。

建议

把测试金字塔用作默认模型,同时了解对它的批评

默认采用大量快速的单元测试、较少的集成测试,以及少量的端到端测试,因为成本和脆弱性会随着测试范围扩大而上升。同时也要了解对这一模型的批评:这个形状应当跟随你的架构,而不是教条。一个以服务为核心的系统,可能需要一个更大的集成测试层(即“测试奖杯”模型),而真正的目标是单位成本和单位速度所换来的信心,而不是某种特定的形状。无论如何,都要避免那种以大量缓慢端到端测试为主的倒金字塔结构。

在有帮助的地方采用 TDD、BDD 和规范驱动开发

使用测试驱动开发(Test-driven development,TDD)来驱动设计并保证可测试性,尤其是对于复杂的逻辑。它既是一种测试纪律,也同样是一种设计纪律。使用行为驱动开发(Behavior-driven development,BDD)以你与相关方共享的领域语言来表达测试,这在受监管或需求繁重的环境中对验收标准很有价值。规范驱动开发更进一步:它把一份可执行的规范(以示例表达的、已达成一致的行为)当作单一的事实来源,既指导实现,又用于验证实现。在需求必须能够追溯到验收证据的场景下(如政府和受监管的项目),这种方法尤为出色。与这三者都相关的是左移测试:把验证工作尽可能提前到生命周期的早期,在编写代码的同时或之前编写测试,并持续运行它们,这样你就能在修复成本最低的时候捕获缺陷,而不是等到后期测试阶段或生产环境中才发现。这三者都不是在所有场景下强制要求的。要在它们能带来清晰性的地方使用它们。

对高价值代码使用高级技术

使用基于属性的测试(property-based testing)来检验在众多生成输入下都成立的不变量,捕获基于示例的测试所遗漏的边界情况。在解析器和不受信任的输入边界上使用模糊测试,以发现崩溃和安全漏洞。使用变异测试来衡量你的测试是否真的能检测出被注入的缺陷,这比原始的覆盖率是一个好得多的质量信号。对序列化输出审慎地使用快照测试,并警惕盲目地重新批准快照这一陷阱。

管理测试数据并使用合成数据

通过受控的、相互隔离的测试数据使测试具有确定性,避免使用把测试彼此耦合在一起的共享可变夹具(fixture)。生成能反映生产环境特征、又不暴露真实个人信息的合成数据,这在隐私规则禁止在测试环境中使用生产数据的场景下是必不可少的。提供工厂(factory)或构建器(builder),使每个测试都能精确地构造出它所需要的数据。

把不稳定的测试当作缺陷来对待

自动检测不稳定性,把不稳定的测试移出阻塞流程,并在限定期限内修复或删除它们。一个会随机失败的测试套件,会训练工程师去忽视失败结果,从而摧毁它的全部价值。要跟踪不稳定率,并把可靠性作为测试套件本身的一项明确质量指标。

把覆盖率当作信号,并加入非功能性测试

度量覆盖率以找出未被测试的区域,但不要把它变成一个会诱使人用无断言测试来投机取巧的硬性指标。用变异测试来补充它,以获得深度。把无障碍性测试(自动化检查加人工审核)、性能测试(带有回归检测的负载和延迟基线),以及安全测试(依赖扫描、静态分析和动态测试)构建进流水线中。

权衡:优点与缺点

测试类型 / 实践优点缺点
单元测试快速、精确、成本低、稳定会遗漏集成和系统级缺陷
集成测试能捕获接口和连接方面的缺陷更慢;需要更多准备工作;更脆弱
端到端测试对真实行为的信心最高缓慢、不稳定、维护成本高
TDD更好的设计、有保证的可测试性有学习曲线;一开始感觉较慢
基于属性的测试发现边界情况,编码不变量需要用“属性”思维思考;编写更难
变异测试对测试有效性的真实度量计算成本高;运行缓慢
高覆盖率目标能发现未被测试的代码可被操纵;可能激励出低价值的测试

这里的核心权衡是信心与速度、成本之间的取舍。范围更广的测试能带来更多信心,但运行更慢、更容易出问题。范围更窄的测试快速而稳定,但会遗漏系统级的缺陷。正确的组合是使每一秒反馈时间、每一小时维护成本所换来的信心最大化。而过度测试也是一种真实存在的失败模式:一个由冗余、缓慢、脆弱的测试组成的臃肿套件,其代价可能超过它所防止的缺陷所带来的代价。

与团队讨论的问题

  1. 哪些非功能性测试()无障碍性、性能和安全()应当阻塞一次发布,哪些只应作为报告存在? 本章主张非功能性测试应当是策略的一部分,而不是事后才想起的补充,并指出无障碍性可能是法律要求,安全测试可能是运营授权证据的一部分。对一个大型或面向公民的系统而言,一道阻塞性关卡会拖慢交付,但一个在生产环境中才被发现的无障碍性或安全缺陷,其修复、声誉和法律代价,远远超过测试本身的代价。请带来决定这一点的信号:你的监管风险敞口、这个系统是否面向公民,以及这些缺陷目前有多频繁地流入生产环境。让法律强制要求的检查具有阻塞性,让风险较低的检查以趋势报告的形式存在,这样这道关卡才能反映真实的风险,而不是教条。这个答案直接决定了什么能够合并、什么不能。

  2. 你们是否设定了一个硬性的覆盖率百分比作为关卡?如果是,什么能阻止工程师用无断言测试来投机取巧? 本章明确指出,覆盖率是一个信号,而不是一个目标;对琐碎代码的高覆盖率说明不了什么;一个硬性目标会诱使人投机取巧。在一个大型组织中强加一个单一的数字,几乎必然会催生出那种执行了代码却不做任何断言的测试,这会提高指标,却降低真实的信心水平。请把一个更好的信号带到讨论现场:在你最高价值模块上的变异测试得分,它衡量的是测试是否真的能检测出被注入的缺陷。用覆盖率来找出未被测试的区域,用变异测试来获得深度,并抵制把两者中的任何一个变成领导层单独追踪的目标。要判断这个数字在哪里真正有帮助,在哪里只是诱发了一场表演。

  3. 当测试套件变得太慢,让工程师无法忍受等待时,你们的应对政策是什么? 本章的核心权衡是信心与速度、成本之间的取舍,并把过度测试列为一种真实存在的失败模式,即一个臃肿、冗余、缓慢的套件,其代价超过它所防止的缺陷。在一个大型团队中,套件的运行时间是每一次改动都要缴纳的共同税,而一个人们学会绕开的套件会失去它全部的价值。请带来证据:CI 的实际耗时、最慢的那些测试,以及有多少冗余的端到端覆盖重复了成本更低的单元测试。把测试推到能够提供真正信心的最低层级,进行并行化,并在限定期限内删除冗余的慢测试。目标是优化每秒反馈时间所换来的信心,而不是原始的测试数量。

  4. 当一个测试变得不稳定时,谁来负责它?必须在多快的时间内修复或删除它?什么机制来强制执行这个期限? 本章把一个不稳定的测试当作一个坏掉的测试、一个一等缺陷来对待,因为一个会随机失败的套件,会训练一个大团队去忽视红色的构建结果,并在无形中摧毁所有人都依赖的这张安全网。这里存在真实的对立压力:把一个不稳定的测试隔离出去,能立即解除对交付的阻塞,但也有掩盖一个真实存在的间歇性缺陷的风险;而对它保持阻塞,则会让数百名工程师因为一个可能纯属噪音的失败而停滞。请带来能够解决这一问题的证据:你当前的不稳定率、测试在被隔离后需要多久才有人处理,以及有多少被隔离的测试最终被证明隐藏了一个真实的缺陷。为每一个被隔离的测试指定一位负责人,设定一个修复或删除的硬性期限,并把可靠性作为该套件本身的一项明确指标来跟踪。在把“构建通过”当作发布证据一部分的企业和政府场景中,一堆无人管理的隔离测试同样是一项审计负债,因为你正在依据一个你私下里已经不再信任的信号来发布产品。

  5. 你们是否被允许在测试环境中使用生产数据?如果不允许,你们将如何生成足够真实、能够捕获真实缺陷的合成数据? 本章明确指出,隐私规则通常禁止在测试中使用真实的个人数据,合成数据必须反映生产环境的特征,否则你的测试只会给出虚假的信心。对一个大型组织而言,这里的张力存在于真实性与合规性之间:生产数据能捕获合成数据所遗漏的混乱边界情况,但每一份生产数据的副本都会成倍增加你的风险敞口和义务。请带来具体信息:哪些数据集携带个人或受监管的数据,你的隐私和数据驻留规则实际要求什么,以及你当前的测试夹具在多大程度上重现了生产环境中观察到的分布和边界情况。把工厂或构建器标准化,使每个测试都能精确地构造出它所需要的数据,并投资于能够匹配真实人口和数据量分布的合成数据生成。在政府和受监管的项目中,在测试环境中使用公民数据不是一条捷径,而是一起需要上报的违规事件,因此数据策略必须在第一个环境搭建起来之前就已经确定。

  6. 在哪些场景下应当要求使用 TDD、BDD 或规范驱动开发,而不是把它们当作可选项?由谁来决定? 本章把这三者呈现为应在能带来清晰性的地方应用的纪律,而不是对每一行代码的强制要求,然而一个大团队会从一个共享的默认做法中受益,这样实践就不会因团队而分裂。这里的权衡存在于设计和可追溯性方面的好处(能被政策专家审阅的可执行规范、能在重构中存活下来的测试)与真实存在的学习曲线和前期的缓慢速度之间,后者会让一刀切的强制要求适得其反。请带来能够界定范围的证据:哪些模块承载着复杂逻辑或较高的变更失败率、哪些地方的验收标准必须能够追溯到需求,以及已经在实践这些方法的团队所报告的速度和缺陷率情况。把这种期望保留给复杂逻辑和需求繁重的领域,让较简单的代码自行选择。在软件必须能够追溯到它所实现的法律的受监管和政府项目中,带有可执行验收证据的规范驱动开发与其说是一种偏好,不如说是通向你的运营授权的一条路径,因此要明确指出哪里是强制要求的。

行业视角

初创企业。 一个小团队无法配备专职 QA,因此要让测试套件物有所值:每次提交都运行快速的单元测试,再加上覆盖那条能带来收入的关键路径的几个端到端测试,不要维护任何你不会真正维护的东西。跳过覆盖率目标,测试那些你最害怕改坏的逻辑,这样你就能每天发布好几次,而不需要手动做一遍回归测试。当天就修复一个不稳定的测试,因为在这个阶段,一个团队学会忽视的测试套件,比根本没有测试套件更糟糕。

小型企业。 没有专职的测试工程师,预算也很紧张,应依靠你已经在使用的框架和工具内置的测试能力,而不是构建一个你无法维护的定制测试框架。优先处理那几项保护收入和客户信任的检查,并使用托管的 CI,这样你就不必自己维护构建基础设施。优先选择购买无障碍性和安全扫描作为一项服务,而不是自己构建它,因为一个被遗漏的缺陷所造成的代价,可能超过这项工具一整年的费用。

企业。 在众多团队之间,策略层面的问题是一致性:一个共享的金字塔默认模型、自动化的不稳定测试隔离机制,以及在各处含义相同的非功能性关卡,这样无论构建是由谁产出的,一次绿色的构建结果都是可信的。要把套件运行时间当作一项共同税来做预算,并大力并行化,因为 CI 的实际耗时是每个工程师在每一次改动上都要支付的成本。要把覆盖率和变异测试得分当作具有明确责任归属的组合层面信号来管理,而不是领导层单独追踪的数字。

政府。 采购和监督使测试不仅仅是一种工程卫生习惯,更是一种证据。要把资格和政策规则表达为经过领域专家审阅的可执行规范,这样你就能把软件追溯到它所实现的法律,并使无障碍性和安全测试具有阻塞性,因为它们是法律要求的,也是运营授权证据的一部分。要使用按照真实分布生成的合成数据,因为在测试环境中使用公民数据是一起需要上报的违规事件,并让测试产物保持可审计,使外部审阅者能够确切地确认究竟验证了什么。

示例

初创企业。 一家五人的初创企业负担不起一个 QA 团队,因此它依靠一个每次提交都会运行的快速单元测试套件,再加上覆盖“注册到结账”这条带来收入的路径的几个端到端测试。创始人们跳过了详尽的覆盖率,转而测试他们最害怕改坏的逻辑,这让他们能够每天发布好几次,而不需要手动做一遍回归测试。当一个测试开始随机失败时,他们当天就修复了它,因为在这个信任就是一切的阶段,一个团队学会忽视的测试套件,比根本没有测试套件更糟糕。

企业。 一个大型电子商务平台维护着数千个快速的单元测试,能在每次提交后的几分钟内运行完毕,还有一组围绕支付和库存边界展开的集中式集成测试,以及一小套针对关键结账流程的端到端测试。不稳定的端到端测试会被自动隔离,并被分配去修复。由于工程师信任这个测试套件,他们每天部署很多次,并确信一次红色的构建结果意味着一个真实存在的问题。

政府。 一个在监管监督下运营的国家福利系统,使用 BDD 把资格规则表达为经过政策专家审阅的可执行规范,从而提供可追溯的证据,证明这个软件实现了相应的法律。它使用按照真实人口分布生成的合成数据,因为隐私规则禁止在测试环境中使用公民数据。无障碍性测试是强制性的,会阻塞发布,因为这项服务必须能够被所有公民使用。安全测试则是运营授权(Authority to Operate,ATO,即在生产环境中运行该系统的正式批准)证据的一部分。

商业案例:动机、投资回报率与总体拥有成本

测试的回报在于能够快速、安全地修改软件,这是持续交付速度的基础。一个值得信赖的自动化测试套件,能取代缓慢、昂贵的手动回归测试,并在修复成本最低的时候()即发布之前,而不是生产环境中()捕获缺陷。在一个受监管或面向公民的系统中,一个生产环境缺陷的代价(修复、声誉和潜在的法律风险)远远超过本可以捕获它的测试的代价。

采纳成本是真实存在的:你需要编写和维护测试,构建持续集成(Continuous integration,CI)基础设施。但不测试的代价更高,而且会不断累积:由恐惧驱动、逐渐陷入停滞的开发方式、频繁的回归问题,以及无法扩展的手动发布流程。过度测试同样有代价,因此这里论证的是一套精心设计的策略,而不是最大数量的测试。要向领导层论证这一点,应把测试套件与部署频率、变更失败率和平均恢复时间关联起来,并量化它所取代的手动测试工作量,以及它所防止的生产事故数量。

反模式与陷阱

  • 冰淇淋筒式测试: 在薄弱的单元测试基础之上,以大量缓慢的端到端测试为主;缓慢、不稳定、成本高昂。
  • 把覆盖率当作目标: 用无断言或琐碎的测试去追逐一个百分比,什么也证明不了。
  • 测试实现细节: 与内部实现耦合的测试,会在每一次重构中被破坏,从而阻碍变更。
  • 容忍不稳定性: 随机的失败训练团队去忽视红色的构建结果。
  • 共享的可变测试数据: 相互干扰、不可预测地失败的测试。
  • 在测试中使用生产数据: 一起随时可能发生的隐私和合规违规事件。
  • 跳过非功能性测试: 无障碍性、性能和安全问题只在生产环境中才被发现。
  • 不被信任的测试套件: 不可靠到工程师习惯性地重新运行或绕过它,使它的目的落空。

成熟度模型

  • 第 1 级,启动(Initiate): 测试是手动且被动的;自动化覆盖率极低;回归问题频繁发生,且往往很晚才被发现,通常是被用户发现,而不是被测试套件发现。
  • 第 2 级,发展(Develop): 存在自动化的单元测试和一些集成测试,但套件运行缓慢或不稳定,信任度低,实践在各团队之间差异很大。
  • 第 3 级,标准化(Standardize): 一个均衡、快速、可靠的测试套件为每一次改动把关;一个已记录的金字塔默认模型、一项不稳定测试处理政策,以及非功能性测试(无障碍性、性能、安全)在各团队中被一致地强制执行。
  • 第 4 级,管理(Manage): 测试套件的健康状况被对照基线进行度量和控制;不稳定率、CI 实际耗时、高价值模块上的变异测试得分,以及流入生产环境的缺陷率被跟踪和评审;覆盖率只是众多信号之一,关卡的触发依据证据,而不是主观意见。
  • 第 5 级,编排(Orchestrate): 高级技术(基于属性的测试、变异测试、模糊测试)被用于高价值代码;测试与部署频率、变更失败率和平均恢复时间等交付指标相集成;组织持续根据自身的架构和风险重塑测试套件,淘汰冗余的测试,并把资源投入到证据显示缺陷仍在流出的地方。

讨论思路

  • 你的测试分布实际呈现出什么形状?它与你的架构和风险相匹配吗?
  • 你如何判断一段代码值得使用基于属性的测试或变异测试,而不是基于示例的测试?
  • 你对不稳定测试的处理政策是什么?它是否真正被强制执行?
  • 你如何在不泄露敏感信息的情况下生成真实可信的合成数据?
  • 覆盖率在哪些地方真正帮到了你?在哪些地方它被拿来投机取巧了?
  • AI 生成的测试应当如何被审阅,才能让它们增加信心,而不是增加噪音?

关键要点

  • 测试是为了获得改动的信心;要优化单位速度和单位成本所换来的信心。
  • 把金字塔模型当作默认做法,但要根据你的架构来塑造测试。
  • 把不稳定的测试当作缺陷来对待,把覆盖率当作信号,而不是目标。
  • 在价值能够证明成本合理的地方,应用高级技术。
  • 把无障碍性、性能和安全测试纳入策略之中,并使用合成数据来保护隐私。

参考文献与延伸阅读

  • Kent Beck, Test-Driven Development: By Example
  • Lisa Crispin and Janet Gregory, Agile Testing: A Practical Guide for Testers and Agile Teams
  • Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • Michael Feathers, Working Effectively with Legacy Code
  • Nicole Forsgren, Jez Humble, Gene Kim, Accelerate: The Science of Lean Software and DevOps
  • Martin Fowler, articles on the Test Pyramid and test-related patterns