第 22 章 测试运行速度的快慢和炽热的岩浆

书中几乎所有的“单元”测试或许都应该叫做整合测试,因为这些测试要不依赖于数据库,要不使用 Django 测试客户端,请求、响应和视图函数之间的中间层有太多细节都被隐藏了。

真正的单元测试一定要隔离,因为单元测试只应该测试软件单独的一部分。

术语:测试的不同类型

  • 隔离测试(纯粹的单元测试)与整合测试

    • 单元测试的主要作用应该是验证应用的逻辑是否正确。隔离测试只能测试一部分代码,测试是否通过与其他任何外部代码都没有关系。纯粹的单元测试是指,对一个函数的测试而言,只有这个函数能让测试失败。如果这个函数依赖于其他系统且破坏这个系统会导致测试失败,就说明这是整合测试。这个系统可以是外部系统,例如数据库,也可以是我们无法控制的另一个函数。不管怎样,只要破坏系统会导致测试失败,这个测试就没有完全隔离,也就不是纯粹的单元测试。整合测试并非不好,只不过可能意味着同时测试两个功能。

  • 集成测试

    • 集成测试用于检查被你控制的代码是否能和你无法控制的外部系统完好集成。集成测试往往也是整合测试。

  • 系统测试

    • 如果说集成测试检查的是与外部系统的集成情况,那么系统测试就是检查应用内部多个系统之间的集成情况。例如,检查数据库、静态文件和服务器配置在一起是否能正常运行。

  • 功能测试和验收测试

    • 验收测试的作用是从用户的角度检查系统是否能正常运行。验收测试很难不写成全栈端到端测试。

22.1 正题:单元测试除了运行速度超快之外还有其他优势

22.1.1 测试运行得越快,开发速度越快

22.1.2 神赐的心流状态

22.1.3 速度慢的测试经常不想运行,导致代码变坏

22.1.4 现在还行,不过随着时间推移,整合测试会变得越来越慢

22.1.5 别只听我一个人说

22.1.6 单元测试能驱使我们实现好的设计

为了编写隔离型好的单元测试,必须知道依赖下一层中的什么功能,而且要使用整合测试无法实现的解耦式架构,这有助于设计出更好的代码。

22.2 纯粹的单元测试有什么问题

22.2.1 隔离的测试难读也难写

22.2.2 隔离测试不会自动测试集成情况

隔离测试只测试当前关注的单元,而且是在隔离的环境中测试,这种测试不测试各单元之间的集成情况。

程序员要记住各单元的界面,要分清每个组件要履行的合约,除了要为单元的内部功能编写测试之外,还得为合约编写测试。

22.2.3 单元测试几乎不能捕获意料之外的问题

单元测试能帮助你捕获差一错误和逻辑混乱导致的错误,这些错误在编写代码时经常会出现,所以这些错误在意料之中。不过出现意料之外的问题时,单元测试不会提醒你。

22.2.4 使用驭件的测试可能和实现方式联系紧密

使用驭件的测试可能和实现方式之间过渡耦合。如果不小心,还可能导致测试本该具有的一个好处缺失,即鼓励重构。如果想修改一个内部 API,你会发现自己要修改很多驭件的测试和合约测试。

注意,处理你无法控制的 API 时,这可并不简单。如果编写的代码完全在自己的控制之中,你可能想设计自己的内部 API,这样写出的代码更简洁,而且测试时不会拐这么多弯。

22.2.5 这些问题都可以解决

22.3 合题:我们到底想从测试中得到什么

22.3.1 正确性

希望应用没有问题,不管是差一错误之类的低层逻辑错误,还是高层问题。想知道是否引入了回归,导致以前能用的功能失效,而且想在用户察觉之前发现。期望测试告诉我们应用可以正常运行。

22.3.2 简洁可维护的代码

希望代码遵守 YAGNI 和 DRY 等原则。希望代码清晰地表明意图,使用合理的方式分成多个组件,且各组件的作用明确、容易理解。期望从测试中获取自信,可以放心地不断重构应用,这样才不会害怕尝试改进设计。还希望测试能主动帮我们找到正确的设计。

22.3.3 高效的工作流程

希望测试能帮助实现一种快速高效的工作流程。希望测试有助于减轻开发压力,而且避免让我们犯一些愚蠢的错误。而且,改进代码时,如果测试不能提供帮助,我们也不想让它成为障碍。

22.3.4 根据所需的优势评估测试

目标

一些考量事项

正确性

- 站在用户的角度看,功能测试的数量是否足够保证应用真的能正常运行? - 各种边界情况彻底测试了吗?感觉这是低层隔离测试的任务。 - 有没有编写测试检查所有组件之间是否能正确配合?要不要编写一些整合测试,或者只用功能测试就行?

简洁可维护的代码

- 测试有没有给我重构代码的自信,而且可以无所畏惧地频繁重构? - 测试有没有帮我得到一个好的设计?如果整合测试较多,隔离测试较少,我要投入精力为应用的哪一部分编写更多隔离测试,才能得到关于设计更全面的反馈?

高效的工作流程

- 反馈循环的速度令我满意吗?我什么时候能得到问题的提醒,有没有某种方法可以让提醒更早出现? - 如果高层功能测试很多,运行时间很长,要花整完时间才能得到意外回归的反馈,有没有一种方法可以让我编写速度更快的测试,整合测试也行,让我早点儿得到反馈? - 如果需要,我能否运行整个测试组件的一个子集? - 我是否花了太多时间等待测试运行完毕,导致高效率的心流状态时间缩短?

22.4 架构方案

还有一些架构方案可以帮助测试组件发挥最大的作用,而且特别有助于避免隔离测试的缺点。

这些架构方案大都要求找到系统的边缘,即代码和外部系统交互的地方,然后尝试将外部系统和应用的核心业务逻辑区分开。

22.4.1 端口和适配器(或六边形、简洁)架构

集成测试在系统的边界,也就是代码和外部系统集成的地方,作用最大。

所以,也就是在边界,隔离测试和驭件的作用最小,因为在边界如果测试和某种实现方式耦合国语紧密,最有可能干扰你,或者在边界需要进一步确认各组件之间是否正确集成。

相反,应用的核心代码(只关注业务逻辑和业务规则的代码,完全在我们控制之中的代码)不太需要整合测试,因为我们能控制也能理解这些代码。

所以,实现需求的一种方法是尽量减少处理边界的代码量。这样,就可以使用隔离测试检查核心业务逻辑,使用整合测试检查集成点。

这种方案称为“端口和适配器”。

编写隔离的单元测试这种模式有时也叫“简洁架构”或“六边形架构”。

22.4.2 函数式核心,命令式外壳

推荐使用他称为“函数式核心,命令式外壳”的架构。应用的“外壳”是边界交互的地方,遵守命令式编程范式,可以使用整合测试、验收测试检查,如果精简到一定程度,甚至完全不用测试。而应用的核心使用函数式编程范式编写(完全没有副作用),因此可以使用完全隔离、纯粹的单元测试,根本无需使用驭件。

22.5 小结

扩展阅读

  • Fast Test、Slow Test 和 Boundaries

  • 端口和适配器

  • 炽热的岩浆

  • 翻转金字塔

  • 整合测试是个骗局

  • 务实的角度

Last updated