等一下,我记得你说过“单元测试必须保证每一个函数的可靠性”这样的话?
对。所以我们必须明确一下单元测试里函数的定义,它跟语言上的函数稍有不同,实际上,是多了一段语义限制:函数,指的是接收0个至多个输入,进行逻辑处理,并产生0个至多个输出的代码块,它的输出受且仅受输入的影响。
符合这种定义的函数,是“可测试函数”,测试它们不需要花额外的精力。不符合这种定义的函数,则是“不可测试函数”。不可测试函数又分两种,一种是无法测试的,比如系统所需的回调函数(尤其是线程、定时器之类的回调)、随机函数等等,它的输出严重依赖于系统当时的状态,而这种状态难以复现,所以对他们的测试可以说是没有任何意义的。另一种是难以测试的,它也是输入-处理-输出这样的,但它的输出除了依赖于输入之外,还依赖于系统的状态,但这种状态是可以复现的。
对于可测试函数,我们没什么好说的,单元测试教材上这样的例子比比皆是。但我们必须说,现实远没有这样理想化,完全不依赖外部环境的函数,非常少见。难道说我们就没有办法做单元测试了不成?
当然也不是这样。但是正如我上面提到,函数并不是天然可测试的。随随便便的拿一个系统就要对它做单元测试,要么是不可能的,要么是难度很大的。为了让一个系统可测试,可单元测试,我们还是要做一些工作的。
最重要的一件工作,当然就是让函数尽可能变成可测试的,再不济也应该是难以测试的,而尽量减少无法测试的函数。这些无法测试的函数,留作功能测试去测(使用人工手段或者其他的测试手段)。
举个例子来说,按上面的定义,线程回调函数是无法测试的,因为它严重依赖调用时的时间点和当时系统状态。如果你把所有的业务逻辑都直接写在回调函数中,那么整个业务逻辑就变成了无法测试的。但是这些逻辑里肯定是有一些是可以剥离出来作为固定输入固定输出的逻辑,那么就把它剥离出来,这样,至少这一部分就变成可测试了,这个经过测试的部分就可以做到一定的保障,减少上一层功能测试的压力。
第二件工作,就是做好Mock Object。纯可测试的函数是很少见的,绝大部分函数都会像上面的那个函数b一样,多多少少要用到一些基础设施,比如时间、随机数、数据库、网络等,这些东西在真实环境中是不稳定的,我们需要构造一个“虚假的”环境,为测试提供一个稳定的基础(稳定的基础当然包括“稳定出错”的情况),这样才可以为测试提供一个稳定结果。这个Mock的工作通常也是很大的,有很多东西需要虚拟,如果是一个小的系统,我们不值得去构造这样一个虚拟环境,可以尽量把其中环境依赖的部分剥离出来,对环境独立的部分单独测试,对环境依赖的部分用人工测试。然而在一个大的系统中,环境依赖部分太多,人工测试变得不可能,这时我们还是有必要认真做一下Mock这个工作的。
当然还有其他的一些工作,包括对软件过程的制定,项目配置,都会随着单元测试的引入而需要做变动,但这不是我们这次讨论的重点,暂且忽略。
说了这么多,总结一下:像显示效果这样的测试,靠单元测试是做不到的,只能用人工或者其他测试手段。但是如果你的系统是按照易于测试的原则实现的话,可以测试那些能够测试的部分,这样会给你的人工测试减轻很多压力,因为很多东西通过单元测试之后,人工测试就可以认为他们一定是正确的了。这就是我前一篇文章所提到的“分层测试”原则。
另外,测试并不是万能良药,对任何系统都可以简单而方便的实施。为了做到易于测试,还是需要在前期(如需求阶段、编码阶段)做一番努力的。
文章来源于领测软件测试网 https://www.ltesting.net/