定义一个好的测试

与所有代码一样,单元测试代码也可以以更好或更差的方式编写。我们已经看到 AAA 如何帮助我们正确构建测试,以及准确、描述性的名称如何讲述我们代码的意图。最有用的测试还遵循 FIRST原则,并且每个测试使用一个断言。

应用 FIRST 原则

这些是使测试更有效的五个原则:

  • 快速(Fast)

  • 隔离(Isolated)

  • 可重复(Repeatable)

  • 自验证(Self-verifying)

  • 及时(Timely)

单元测试需要快速,就像我们之前的示例一样。这对于测试优先的 TDD 尤其重要,因为我们在探索设计和实现时希望获得即时反馈。如果我们运行一个单元测试,即使它只需要 15 秒完成,我们也会很快减少运行测试的频率。我们将退化为编写大块的生产代码而不进行测试,以减少等待缓慢测试完成的时间。这与我们从 TDD 中期望的完全相反,因此我们努力保持测试的快速性。我们需要单元测试在 2 秒或更短时间内运行,理想情况下是毫秒级。即使是 2 秒也是一个相当高的数字。

测试需要彼此隔离。这意味着我们可以选择任何测试或任何测试组合,并以我们喜欢的任何顺序运行它们,并且始终获得相同的结果。一个测试不能依赖于之前运行的另一个测试。这通常是未能编写快速测试的症状,因此我们通过缓存结果或安排步骤设置来弥补。这是一个错误,因为它会减慢开发速度,尤其是对我们的同事而言。原因是我们不知道测试必须运行的特殊顺序。当我们单独运行任何测试时,如果它没有正确隔离,它将失败并产生假阴性结果。该测试不再告诉我们有关被测代码的任何信息。它只告诉我们我们没有在它之前运行某些其他测试,而没有告诉我们可能是哪个测试。隔离对于健康的 TDD 工作流程至关重要。

可重复的测试对 TDD 至关重要。每当我们使用相同的生产代码运行测试时,该测试必须始终返回相同的通过或失败结果。这听起来可能很明显,但需要小心实现。想象一下,一个测试检查一个返回 1 到 10 之间随机数的函数。如果我们断言返回数字 7,即使我们正确编写了函数,该测试也只会偶尔通过。在这方面,三个常见的痛苦来源是涉及数据库的测试、针对时间的测试和通过用户界面的测试。我们将在第 8 章 “测试替身——Stubs 和 Mocks” 中探讨处理这些情况的技术。

所有测试都必须是自验证的。这意味着我们需要可执行代码来运行并检查输出是否符合预期。此步骤必须自动化。我们不能将此检查留给手动检查,例如通过将输出写入控制台并让人根据测试计划进行检查。单元测试从自动化中获得了巨大的价值。计算机检查生产代码,使我们免于遵循测试计划的繁琐、人类活动的缓慢以及人为错误的可能性。

及时的测试是在最有用的时候编写的测试。编写测试的理想时间是在编写使测试通过的代码之前。看到团队使用不太有益的方法并不罕见。最糟糕的方法当然是永远不编写任何单元测试,而依赖手动 QA 来发现错误。通过这种方法,我们无法获得任何设计反馈。另一个极端是让分析师提前为组件——甚至整个系统——编写每个测试,将编码变成机械练习。这也无法从设计反馈中学习。它还可能导致过度指定的测试,锁定不良的设计和实现选择。许多团队从编写一些代码开始,然后继续编写单元测试,从而错过了早期设计反馈的机会。它还可能导致未经测试的代码和有缺陷的边缘情况处理。

我们已经看到 FIRST 原则如何帮助我们专注于编写一个好的测试。另一个重要的原则是不要试图一次测试太多内容。如果我们这样做,测试将变得非常难以理解。一个简单的解决方案是每个测试只写一个断言,我们将在接下来讨论这一点。

每个测试使用一个断言

当测试简短且具体时,它们提供最有用的反馈。它们就像显微镜一样作用于代码,每个测试都突出了我们代码的一个小方面。确保这一点发生的最佳方法是每个测试只写一个断言。这可以防止我们在一个测试中处理太多内容。这使我们能够专注于测试失败时收到的错误消息,并帮助我们控制代码的复杂性。它迫使我们进一步分解问题。

决定单元测试的范围

另一个常见的误解是单元测试中的 “单元” 是什么意思。单元指的是测试的隔离性本身——每个测试都可以被视为一个独立的单元。因此,只要测试可以独立运行,被测代码的大小可以有很大的变化。

将测试本身视为单元,统一了关于单元测试范围的几种流行观点。通常,人们说单元是最小的可测试代码——一个函数、方法、类或包。这些都是有效的选项。另一个常见的论点是,单元测试应该是类测试——每个生产代码类对应一个单元测试类,每个生产方法对应一个单元测试方法。虽然这很常见,但这通常不是最好的方法。它不必要地将测试的结构与实现的结构耦合在一起,使得代码在未来更难更改,而不是更容易。

单元测试的理想目标是覆盖一个外部可见的行为。这适用于代码库中的多个不同规模。我们可以跨多个类包对整个用户故事进行单元测试,前提是我们能够避免操作外部系统,如数据库或用户界面。我们将在第 9 章 “六边形架构——解耦外部系统” 中探讨实现这一目标的技术。我们还经常使用更接近代码细节的单元测试,仅测试单个类的公共方法。

一旦我们根据我们希望代码具有的设计编写了测试,我们就可以专注于测试的更明显方面:验证我们的代码是否正确。