本节书摘来自华章出版社《有效的单元测试》一书中的第2章,第2.4节,作者 (芬)Lasse Koskela,更多章节内容可以访问云栖社区“华章计算机”公众号查看
2.4 独立的测试易于单独运行
关于测试有很多话要说,哪些该包含,哪些不该包含,哪些该指定,哪些不该指定,如何从可读性角度来组织,等等。对测试外围的考虑有时也起了至关重要的作用。
人类——我们的大脑过于精确——是极其强大的信息处理器。我们几乎可以瞬间评估身体周围的环境并在眨眼间做出反应。我们能在意识到雪球飞过来之前就做出闪避。这些反应根植于我们的DNA中。
当我们感知到相似模式时,行为图谱就会指示身体移动。随着时间的推移,这些食谱慢慢变得成熟,我们很快就背负上了一个互联模式和行为的复杂网络。
这种情况也发生在工作中。首次探索别人的代码库时,我们会在15分钟内形成对常见惯例、模式、代码坏味道和陷阱的清晰图景。这是我们识别相似模式的能力在起作用,并且能够告诉我们可能还会在附近看到哪些其他东西。
代码坏味道是什么?
代码中的坏味道提示我们代码中某些地方可能出问题了。引用Portland Pattern Repository的Wiki,“如果某些东西闻起来发臭了,那绝对需要检查它一下,但是不见得真的需要修复它,或者只能继续忍受。”
例如,当我接触新的代码库时,我注意到的第一件事情就是方法的大小。如果方法过大,我立马明白在那些特定模块、组件或源文件中还有一大堆问题等着我呢。我关注的另一个信号是变量、类和方法名字的描述性如何。
具体说到测试代码,我关注测试的独立水平,尤其是架构边界附近。这样做是因为我在边界上仔细发现了许多代码坏味道,于是我学会了一看到外部依赖时就特别小心,包括:
时间
随机数
并发性
基础设施
现存数据
持久化
网络
这些事物的共同之处在于它们往往都很复杂,对于一个项目的测试基础设施(infrastructure)来说,我认为最基本的试金石(litmus test)就是:我能否从版本控制中签出全新的代码,复制到刚刚打开包装的新计算机上,运行一条命令,然后翘起二郎腿,看着整套自动化测试运行并且通过?
隔离和独立很重要,因为没有它们就难以运行和维护测试。开发者为了运行测试而不得不对系统做的每件事都会使事情变得更加繁琐。
无论你是否需要在文件系统中特定位置创建空目录,或确保你具备一个特定版本的MySQL运行在特定端口号上,或添加一条用于测试用户登录的数据库用户记录,或设定一堆环境变量——这些都不是开发者该做的。这些小事增加了工作量并会累积为奇怪的测试失败。
例如测试执行时的系统时钟或随机数生成器的下一个值,这些都不在你的控制之中,而这正是此类依赖的特征。作为经验法则,你想要避免由于这种依赖而导致测试古怪地失败。你希望将代码放进一个台钳,通过传入测试替身或者将代码与环境隔离,使其行为符合你的需要,从而控制一切。
在测试类中不要依赖于测试的顺序
一般来说,不让测试互相依赖是指你不该让一个类中的测试依赖另一个类中测试的执行或结果。但这也同样适用于同一个测试类中的依赖。
该错误的典型例子是这样的,当程序员在@BeforeClass方法中设置系统的起始状态后,写下了三个连贯的@Test方法,每个都在修改系统状态,并相信上一个测试完成了一部分工作。现在,当第一个测试失败时,后面所有测试都会失败,但那还不是最大的问题——至少提示你发现了错误,对吗?
真正的问题是当其中某些测试因为错误的原因而失败。例如,假设测试框架决定以不同的顺序来调用测试方法。虚惊一场。JVM供应商决定改变反射API返回方法的顺序。一场虚惊。测试框架作者决定以字母顺序来运行测试。又是虚惊一场。
你不喜欢总是一惊一乍的。当测试要检查的行为正常时,你并不希望你的测试失败。因此,你不该故意让测试执行相互依赖而造成它们很脆弱。
测试意外失败的最不寻常的例子之一,是一个测试作为套件的一部分时可以通过,但单独运行却神秘地失败(反之亦然)。
那些症状散发着测试相互依赖的臭气。它们假设另一个测试在自己之前运行,而且那个测试会将系统置于某个特定状态。当假设不成立时,你就硬着头皮去调试吧。
总而言之,当编写的测试涉及时间、随机数、并发性、基础设施、持久化或网络时,你就应该格外小心。作为经验来说,你应该尽量避免依赖它们,将它们限制到小的隔离单元中,这样你的大部分测试就不会遭受并发症,也不用总是挨个处理它们——只有少数几个地方才用得着操心。
那么在实践中看起来如何呢?你到底该做什么?例如,你可以看看你能否找到一个方式来做下面这些事:
用测试替身替换对第三方库的依赖,根据需要将其包装到你自己的适配层中。将各种麻烦封装进适配层以后,你就可以独立地测试其余的程序逻辑。
将测试代码与其用到的资源放在一起,或许是在一个包(package)里。
让测试代码自己产生所需资源,而不要让它们与源代码分开。
令测试自行建立所需的上下文。不要依赖于之前运行的任何测试。
对于需要持久化的集成测试,那就使用内存数据库吧,用了干净的数据集,就能极大地简化测试的启动问题。还有,它们通常启动得超级快。
将线程代码分为同步和异步两部分,所有程序逻辑都放在一个常规的同步代码单元中,就可以方便地进行测试并且没有并发症,将棘手的并发部分留给一小堆专用测试。
当面对遗留代码时要做到测试隔离是很难的,那些代码在设计时并未考虑可测试性,因此不具备你想要的模块化。但即使这样,仍然值得去打破那些讨厌的依赖从而使你的测试与环境隔离并相互独立。你的测试毕竟得靠得住才行。