单元测试

2014年5月5日

单元测试在软件开发中经常被提及,也是我在编写程序期间一直熟悉的术语。然而,就像大多数软件开发术语一样,它定义非常模糊,我发现人们经常混淆,认为它的定义比实际更严格。 [1]

虽然我之前做过很多单元测试,但我对它的明确接触是在我开始与肯特·贝克合作并使用 Xunit 系列单元测试工具时。(事实上,我有时认为这种测试风格的一个好名称可能是“xunit 测试”。)单元测试也成为 极限编程 (XP) 的一项标志性活动,并迅速导致了 测试驱动开发

从一开始,人们就对 XP 使用单元测试的定义存在争议。我清楚地记得在一个 usenet 讨论组中,我们这些 XP 倡导者被一位测试专家斥责为误用“单元测试”一词。我们问他他的定义,他回答说:“在我的培训课程的第一天早上,我会讲解 24 种不同的单元测试定义。”

尽管存在差异,但有一些共同点。首先,单元测试被认为是低级别的,专注于软件系统的一小部分。其次,如今单元测试通常由程序员自己使用他们常用的工具编写——唯一的区别是使用某种单元测试框架 [2]。第三,单元测试预计比其他类型的测试要快得多。

因此,有一些共同点,但也存在差异。一个差异是人们认为什么是单元。面向对象设计倾向于将类视为单元,而过程式或函数式方法可能将单个函数视为单元。但实际上这是一个具体情况具体分析的问题——团队决定什么对他们理解系统及其测试来说是合理的单元。虽然我从将单元视为类的概念开始,但我经常将一组密切相关的类视为一个单元。很少情况下,我可能会将类中的一部分方法视为一个单元。无论你如何定义它,实际上并不重要。

孤独还是合群?

一个更重要的区别是,你正在测试的单元应该是合群的还是孤独的 [3]。假设你正在测试订单类中的价格方法。价格方法需要调用产品类和客户类的一些函数。如果你希望你的单元测试是孤独的,你就不想在这里使用真实的 product 或 customer 类,因为 customer 类中的错误会导致订单类的测试失败。相反,你应该使用 测试替身 来代替协作者。

但并非所有单元测试人员都使用孤独的单元测试。事实上,当 xunit 测试在 90 年代开始时,我们没有尝试过使用孤独的测试,除非与协作者的通信很麻烦(例如远程信用卡验证系统)。我们发现追踪实际错误并不困难,即使它会导致相邻的测试失败。因此,我们认为允许我们的测试合群不会在实践中导致问题。

事实上,使用合群的单元测试是我们被批评使用“单元测试”一词的原因之一。我认为“单元测试”一词是合适的,因为这些测试是对单个单元行为的测试。我们编写测试时假设除了该单元之外的所有内容都正常工作。

随着 xunit 测试在 2000 年代变得越来越流行,孤独测试的概念又回来了,至少对某些人来说是如此。我们看到了模拟对象和支持模拟的框架的兴起。出现了两种 xunit 测试学派,我称之为经典风格和模拟风格。两种风格之间的一个区别是,模拟主义者坚持使用孤独的单元测试,而经典主义者则更喜欢合群的测试。今天,我认识并尊重这两种风格的 xunit 测试人员(就我个人而言,我一直坚持经典风格)。

即使像我这样的经典测试人员也会在协作很麻烦时使用测试替身。它们对于消除 与远程服务通信时的非确定性 非常宝贵。事实上,一些经典主义 xunit 测试人员还认为,任何与外部资源(例如数据库或文件系统)的协作都应该使用替身。部分原因是由于非确定性风险,部分原因是由于速度。虽然我认为这是一个有用的指导原则,但我不会将使用替身来代替外部资源视为绝对规则。如果与资源的通信稳定且速度足够快,那么就没有理由不在单元测试中这样做。

速度

单元测试的共同特性——范围小、由程序员自己完成、速度快——意味着它们可以在编程时非常频繁地运行。事实上,这是 自测试代码 的关键特征之一。在这种情况下,程序员会在对代码进行任何更改后运行单元测试。我可能每分钟运行几次单元测试,只要我有值得编译的代码。我这样做是因为,如果我不小心破坏了某些东西,我希望立即知道。如果我在最后一次更改时引入了缺陷,那么我更容易发现错误,因为我不需要查找太多内容。

当你频繁运行单元测试时,你可能不会运行所有单元测试。通常,你只需要运行那些针对你当前正在处理的代码部分的测试。像往常一样,你用测试深度来换取运行测试套件所需的时间。我将这个套件称为编译套件,因为这是我在想到编译时运行的——即使是在像 Ruby 这样的解释型语言中。

如果你正在使用持续集成,你应该在其中运行一个测试套件。这个套件(我称之为提交套件)通常包含所有单元测试。它还可能包含一些 广域堆栈测试。作为一名程序员,你应该每天运行几次这个提交套件,当然是在任何共享提交到版本控制之前,但也要在你有机会的时候运行——当你休息或去参加会议时。提交套件越快,你就可以越频繁地运行它。 [4]

不同的人对单元测试及其测试套件的速度有不同的标准。 大卫·海涅迈尔·汉森 对编译套件需要几秒钟,而提交套件需要几分钟感到满意。 加里·伯恩哈特 发现这太慢了,他坚持编译套件大约需要 300 毫秒,而 丹·博达特 不希望他的提交套件超过 10 秒。

我认为这里没有绝对的答案。就我个人而言,我没有注意到亚秒级编译套件和几秒钟编译套件之间的区别。我喜欢肯特·贝克的经验法则,即提交套件的运行时间不应超过 10 分钟。但真正重要的是,你的测试套件应该运行得足够快,以至于你不会被阻止频繁运行它们。而频繁运行意味着,当它们检测到错误时,需要查看的工作量足够少,以便你能够快速找到它。

注释

1: 我在 集成测试 条目中简要介绍了该名称的历史渊源。

2: 我说“如今”,因为这确实是由于 XP 而发生的变化。在世纪之交的辩论中,XP 倡导者因这一点而受到强烈批评,因为普遍的观点是程序员不应该测试自己的代码。一些公司有专门的单元测试人员,他们的全部工作就是为开发人员之前编写的代码编写单元测试。这样做的原因包括:人们对测试自己的代码存在概念上的盲点,程序员不是优秀的测试人员,以及在开发人员和测试人员之间保持对抗关系是件好事。XP 倡导者的观点是,程序员可以学会成为有效的测试人员,至少在单元级别上,并且如果你让一个独立的团队参与进来,那么测试给你的反馈循环将变得非常缓慢。Xunit 在这里发挥了至关重要的作用,它专门设计用于最大程度地减少程序员编写测试时的摩擦。

3: 杰伊·菲尔兹 提出了“孤独”和“合群”这两个术语。

4: 如果你有一些有用的测试,但它们运行时间比你希望的提交套件运行时间更长,那么你应该构建一个 部署管道,并将较慢的测试放在管道中的后期阶段。

修订

2014 年 10 月 24 日更新,包括对菲尔兹的合群/孤独词汇的提及。

2017 年 3 月 9 日更新,将合群/孤独术语作为描述区别的主要方式,并删除了“协作者隔离”一词的使用(由于与隔离测试夹具更改相互之间混淆)。