使用单元测试进行左移测试

测试有助于确保代码按预期执行,但构建测试的时间和精力会占用其他任务(如功能开发)的时间。 考虑到这个代价,从测试中获得最大价值是很重要的。 本文讨论了 DevOps 测试原则,重点讨论了单元测试的价值和左移测试策略。

专用测试人员曾经编写大多数测试,许多产品开发人员没有学会编写单元测试。 编写测试可能看起来太难,或者工作量太大。 可能会有人怀疑单元测试策略是否有效,有人对写得不好的单元测试有不好的体验,或者担心单元测试会取代功能测试。

Graphic that describes arguments about adopting unit testing.

要实施 DevOps 测试策略,要务实并注重建立势头。 尽管您可以坚持对可以干净重构的新代码或现有代码进行单元测试,但对于遗留代码库来说,允许一些依赖性可能是有意义的。 如果产品代码的重要部分使用 SQL,那么允许单元测试依赖于 SQL 资源提供程序而不是模拟该层可能是一种短期的进步方法。

随着 DevOps 组织的成熟,领导层更容易改进流程。 虽然变革可能会遇到一些阻力,但敏捷组织重视那些明显会带来回报的变革。 销售更快的测试运行、更少的失败的愿景应该很容易,因为这意味着有更多的时间投资于通过功能开发产生新的价值。

DevOps 测试分类

定义测试分类法是 DevOps 测试过程的一个重要方面。 DevOps 测试分类法根据依赖关系和运行时间对单个测试进行分类。 开发人员应该了解在不同场景中使用的正确类型的测试,以及流程的不同部分需要哪些测试。 大多数组织将测试分为四个级别:

  • L0L1 测试是单元测试,或者依赖于被测程序集中的代码而不依赖于其他测试的测试。 L0 是一类广泛的快速内存单元测试。
  • L2功能测试,可能需要程序集和其他依赖项,如 SQL 或文件系统。
  • L3 功能测试针对可测试的服务部署运行。 此测试类别需要服务部署,但可能会使用存根作为关键服务依赖项。
  • L4 测试是针对生产运行的一类受限集成测试。 L4 测试需要完整的产品部署。

虽然所有测试都能一直运行是理想的,但这是不可行的。 团队可以选择 DevOps 流程中运行每个测试的位置,并使用左移右移策略在流程的早期或后期移动不同的测试类型。

例如,预期可能是开发人员在提交之前总是运行 L2 测试,如果 L3 测试运行失败,拉取请求会自动失败,如果 L4 测试失败,部署可能会被阻止。 具体的规则可能因组织而异,但对组织内所有团队的期望都会使每个人朝着相同的质量愿景目标前进。

单元测试指南

为 L0 和 L1 单元测试设置严格的指导原则。 这些测试需要非常快速可靠。 例如,程序集中每个 L0 测试的平均执行时间应小于 60 毫秒。 程序集中每个 L1 测试的平均执行时间应小于 400 毫秒。 此级别的任何测试都不应超过 2 秒。

Microsoft 的一个团队在不到六分钟的时间里并行运行了 60,000 多个单元测试。 他们的目标是将这段时间减少到一分钟以内。 该团队使用下图等工具跟踪单元测试执行时间,并针对超过允许时间的测试归档 bug。

Chart that shows continuous focus on test execution time.

功能测试指南

功能测试必须是独立的。 L2 测试的关键概念是隔离。 适当隔离的测试可以在任何序列中可靠地运行,因为它们可以完全控制运行环境。 在测试开始时必须知道状态。 如果一个测试创建了数据并将其留在数据库中,则可能会破坏另一个依赖于不同数据库状态的测试的运行。

需要用户标识的旧测试可能已调用外部身份验证提供程序来获取标识。 这种做法带来了一些挑战。 外部依赖可能不可靠或暂时不可用,从而破坏测试。 这种做法也违反了测试隔离原则,因为测试可能会更改标识的状态,例如权限,从而导致其他测试出现意外的默认状态。 考虑通过在测试框架内投资于身份支持来防止这些问题。

DevOps 测试原则

为了帮助将测试组合过渡到现代 DevOps 流程,请阐明质量愿景。 团队在定义和实施 DevOps 测试策略时应遵守以下测试原则。

Diagram that shows an example of a quality vision and lists test principles.

左移以更早进行测试

测试可能需要很长时间才能运行完毕。 随着项目规模的扩大,测试数量和类型也在大幅增长。 当测试套件需要数小时或数天才能完成时,它们可以进一步扩展,直到最后一刻运行。 测试的代码质量优势直到代码提交很久之后才能实现。

长时间运行的测试也可能产生耗时的故障。 团队可以建立对失败的容忍度,尤其是在冲刺的早期。 这种容忍度破坏了测试作为深入了解代码库质量的价值。 长时间运行、最后一刻的测试也增加了冲刺结束预期的不可预测性,因为必须支付未知数量的技术债务才能使代码可交付。

左移测试的目标是通过在管道更早执行测试提升质量。 通过测试和流程改进的结合,左移既减少了测试运行所需的时间,也减少了周期后期失败的影响。 左移可确保在变更合并到主分支之前完成大多数测试。

Diagram that shows the move to shift-left testing.

除了将某些测试职责转移到左边以提高代码质量之外,团队还可以将其他测试方面转移到右边,或者在 DevOps 周期的后期,以改进最终产品。 有关详细信息,请参见右移以在生产中进行测试

以尽可能低的级别编写测试

编写更多的单元测试。 倾向于使用最少外部依赖项的测试,并将重点放在作为构建的一部分运行大多数测试上。 考虑一个并行生成系统,该系统可以在程序集和相关联的测试结束后立即运行程序集的单元测试。 在这个级别上测试服务的各个方面是不可行的,但原则是使用较轻的单元测试,如果它们能产生与较重的功能测试相同的结果。

以测试可靠性为目标

不可靠的测试在组织上的维护成本很高。 这样的测试直接违背了工程效率目标,因为它很难自信地做出更改。 开发人员应该能够在任何地方进行更改,并迅速获得信心,认为没有任何东西被破坏。 保持较高的可靠性标准。 不鼓励使用 UI 测试,因为它们往往不可靠。

编写可以在任何地方运行的功能测试

测试可能使用专门为实现测试而设计的专用集成点。 这种做法的一个原因是产品本身缺乏可测试性。 不幸的是,像这样的测试通常依赖于内部知识,并使用从功能测试角度来看无关紧要的实现细节。 这些测试仅限于具有运行测试所需的机密和配置的环境,这通常不包括生产部署。 功能测试应仅使用产品的公共 API。

为可测试性设计产品

在成熟的 DevOps 流程中,组织可以全面了解在云平台上交付高质量产品的意义。 将平衡有力地转向单元测试而非功能测试,需要团队做出支持可测试性的设计和实现选择。 对于什么是设计良好、实现良好的可测试性代码,有不同的想法,就像有不同的编码风格一样。 原则是,可测试性设计必须成为关于设计和代码质量讨论的主要部分。

将测试代码视为产品代码

明确说明测试代码就是产品代码,可以清楚地表明测试代码的质量与产品代码的质量对运输同样重要。 团队应该像对待产品代码一样对待测试代码,并对测试和测试框架的设计和实现给予同等的关注。 这项工作类似于将配置和基础结构作为代码进行管理。 为了完成,代码审查应该考虑测试代码,并将其与产品代码保持在相同的质量标准。

使用共享测试基础结构

降低使用测试基础结构生成可靠质量信号的门槛。 将测试视为整个团队的共享服务。 将单元测试代码与产品代码一起存储,并与产品一起构建。 作为构建过程的一部分运行的测试也必须在 Azure DevOps 等开发工具下运行。 如果测试可以在从本地开发到生产的每个环境中运行,那么它们就具有与产品代码相同的可靠性。

让代码所有者负责测试

测试代码应该位于存储库中的产品代码旁边。 对于要在组件边界测试的代码,将测试责任推给编写组件代码的人员。 不要依赖他人来测试组件。

案例研究:使用单元测试左移

Microsoft 的一个团队决定用现代的 DevOps 单元测试和左移流程取代他们的遗留测试套件。 该团队跟踪了每三周一次的冲刺进度,如下图所示。 该图涵盖了 78-120 的冲刺,代表了 126 周内的 42 次冲刺,即大约两年半的工作量。

该团队在冲刺 78 中从 27K 遗留测试开始,在 S120 中达到零遗留测试。 一组 L0 和 L1 单元测试取代了大多数旧的功能测试。 新的 L2 测试取代了一些测试,许多旧的测试也被删除了。

Diagram that shows a sample test portfolio balance over time.

在一个需要两年多时间才能完成的软件之旅中,有很多东西需要从过程本身学习。 总的来说,在两年内彻底重做测试系统的努力是一项巨大的投资。 并不是每个功能团队都在同一时间完成这项工作。 组织中的许多团队在每次冲刺中都投入了时间,在某些冲刺中,这是团队所做的大部分工作。 尽管很难衡量转变的成本,但这对团队的质量和绩效目标来说是一个不可谈判的要求。

使用入门

一开始,该团队将称为 TRA 测试的旧功能测试单独留下。 该团队希望开发人员接受编写单元测试的想法,特别是对于新功能。 重点是使 L0 和 L1 测试的编写尽可能简单。 团队需要首先发展这种能力,并建立势头。

上图显示了单元测试计数开始提前增加,因为团队看到了编写单元测试的好处。 单元测试更容易维护,运行速度更快,故障更少。 很容易获得在拉取请求流中运行所有单元测试的支持。

直到冲刺 101,团队才专注于编写新的 L2 测试。 与此同时,从冲刺 78 到冲刺 101,TRA 测试次数从 27,000 次下降到 14,000 次。 新的单元测试取代了一些 TRA 测试,但根据团队对其有用性的分析,许多测试被简单地删除了。

在冲刺 110 中,TRA 测试从 2,100 跳到 3,800,因为在源树中发现了更多的测试并添加到了图中。 事实证明,这些测试一直在运行,但没有得到正确的跟踪。 这不是一场危机,但重要的是要诚实,并根据需要重新评估。

更快

一旦团队获得了快速可靠的持续集成 (CI) 信号,它就成为了产品质量的可靠指标。 下面的屏幕截图显示了拉取请求和 CI 管道的操作,以及经过各个阶段所需的时间。

Diagram that shows the pull request and rolling CI pipeline in action.

从拉取请求到合并大约需要 30 分钟,其中包括运行 60,000 个单元测试。 从代码合并到 CI 构建大约需要 22 分钟。 来自 CI 的第一个高质量信号 SelfTest 在大约一个小时后发出。 然后,用提议的更改对大部分产品进行测试。 在从 Merge 到 SelfHost 的两个小时内,整个产品都经过了测试,并准备投入生产。

使用指标

团队跟踪记分卡,如下例所示。 在高水平上,记分卡跟踪两种类型的指标:健康或债务,以及速度。

Diagram that shows a metrics scorecard for tracking test performance.

对于现场健康指标,团队跟踪检测时间、缓解时间以及团队携带的维修项目数量。 维修项目是团队在现场回顾中确定的工作,以防止类似事件再次发生。 记分卡还跟踪团队是否在合理的时间内完成维修项目。

对于工程健康指标,团队跟踪每个开发人员的活动 bug。 如果一个团队的每个开发人员有五个以上的 bug,那么该团队必须在开发新功能之前优先修复这些 bug。 该团队还跟踪安全等特殊类别中的老化漏洞。

工程速度度量衡量连续集成和连续交付(CI/CD)管道不同部分的速度。 总体目标是提高 DevOps 管道的速度:从一个想法开始,将代码投入生产,并从客户那里接收数据。

后续步骤