技术开发 频道

再谈单元测试

【IT168 技术文章】

  今天收到一封信,问了我一个问题:

  关于你提出的几点:1. 单元测试是一种测试,它不是代码的一部分;2. 单元测试是最低层级的测试,它只保证函数的可靠性,不保证其它;3. 单元测试应该能保证每一个函数的可靠性。

  当今前端测试的问题在于仅仅对函数的输出进行验证并不能很好的确认其行为。因为js还需要对DOM进行操作,需要对CSS进行操作,IE,FF显示效果不一致等等。使得前端开发程序员不得不人肉进行测试,查看程序是否符合预期的显示效果。你们认为如何才能提高前端单元测试的有效性呢?

  说实话这个问题是我刚刚接触单元测试的时候,也一直被困扰的一个问题,那就是,GUI界面如何单元测试?我记得在几年前,我还就这个问题特地咨询过gigix,当时他告诉我说“测试能测试的”。但是当时我对单元测试一知半解的时候,对于这个答案也是不甚了了。

  今天我不敢说对这个问题已经理解得非常透彻了,但是我想把我的想法说出来,大家讨论一下。

  在解释这个问题之前,我想重复一下我对单元测试的理解:单元测试是最底层的测试,它只保证函数的可靠性。

  但是这里有一个重要的概念,我们需要进一步的说两句:什么叫“函数”?

  从语法而言,函数就是语言概念上的一个语句块,这个语句块接收0个至多个输入,产生0个至多个输出。但是,所有的语言都没有规定函数需要有怎样的“语义”。于是,我们也不能在解释器或编译器层面阻止一个“坏函数”的诞生。例如,下面这两个函数,如何测试?

x = 1;

function a()
{
   global x = x+1;
   if (x < 10)
       setTimer(100, b);
}
function b()
{
    feed = time.now();
    diff = random(feed).getInteger(10);
    global x = x - diff;
}

 


  函数a严重依赖于函数b,以及平台相关的定时器和一个状态未知的全局变量x。而函数b也依赖于平台相关的函数time和一个状态未知的全局变量x。这样两个函数要进行测试,难度是非常高的。简单的说,a几乎可以认为是无法测试的,因为它并不是一个我们所谓的“输入-处理-输出”函数,而是依赖于定时器这样的平台相关操作,定时器这种东西是很难模拟的,就算模拟出来意义也不大,因为真实的定时器几乎可以肯定不会跟模拟的定时器有相同的表现——而这个表现,正是我们编写这个函数的目的。函数b倒是可以测试,但你必须事先为它模拟好一个时间函数(类),一个随机数函数(类),和一个确定状态的全局变量x——这些工作在一些语言里可以做到但要付出很大的代价,在另一些语言里几乎可以说是不可能的任务。

  那么,接下来的工作倒也变得很简单了:我们就要做一个价值上的衡量,我花很多时间精力去实现这些测试基础框架(对了,这些基础框架也需要测试),跟我整个系统的测试工作本身相比到底值不值?

  其实在大部分时候,这个答案是“不值”。如果为了一个系统的单元测试要做如此多的工作,其难度不亚于开发一个新系统,那我们当然会选择不测试。

  上面的例子说明了两点:1、函数并不是天然可测试的;2、不是所有的“函数”都是需要测试的。

  等一下,我记得你说过“单元测试必须保证每一个函数的可靠性”这样的话?

  对。所以我们必须明确一下单元测试里函数的定义,它跟语言上的函数稍有不同,实际上,是多了一段语义限制:函数,指的是接收0个至多个输入,进行逻辑处理,并产生0个至多个输出的代码块,它的输出受且仅受输入的影响。

  符合这种定义的函数,是“可测试函数”,测试它们不需要花额外的精力。不符合这种定义的函数,则是“不可测试函数”。不可测试函数又分两种,一种是无法测试的,比如系统所需的回调函数(尤其是线程、定时器之类的回调)、随机函数等等,它的输出严重依赖于系统当时的状态,而这种状态难以复现,所以对他们的测试可以说是没有任何意义的。另一种是难以测试的,它也是输入-处理-输出这样的,但它的输出除了依赖于输入之外,还依赖于系统的状态,但这种状态是可以复现的。

  对于可测试函数,我们没什么好说的,单元测试教材上这样的例子比比皆是。但我们必须说,现实远没有这样理想化,完全不依赖外部环境的函数,非常少见。难道说我们就没有办法做单元测试了不成?

  当然也不是这样。但是正如我上面提到,函数并不是天然可测试的。随随便便的拿一个系统就要对它做单元测试,要么是不可能的,要么是难度很大的。为了让一个系统可测试,可单元测试,我们还是要做一些工作的。

  最重要的一件工作,当然就是让函数尽可能变成可测试的,再不济也应该是难以测试的,而尽量减少无法测试的函数。这些无法测试的函数,留作功能测试去测(使用人工手段或者其他的测试手段)。

  举个例子来说,按上面的定义,线程回调函数是无法测试的,因为它严重依赖调用时的时间点和当时系统状态。如果你把所有的业务逻辑都直接写在回调函数中,那么整个业务逻辑就变成了无法测试的。但是这些逻辑里肯定是有一些是可以剥离出来作为固定输入固定输出的逻辑,那么就把它剥离出来,这样,至少这一部分就变成可测试了,这个经过测试的部分就可以做到一定的保障,减少上一层功能测试的压力。

  第二件工作,就是做好Mock Object。纯可测试的函数是很少见的,绝大部分函数都会像上面的那个函数b一样,多多少少要用到一些基础设施,比如时间、随机数、数据库、网络等,这些东西在真实环境中是不稳定的,我们需要构造一个“虚假的”环境,为测试提供一个稳定的基础(稳定的基础当然包括“稳定出错”的情况),这样才可以为测试提供一个稳定结果。这个Mock的工作通常也是很大的,有很多东西需要虚拟,如果是一个小的系统,我们不值得去构造这样一个虚拟环境,可以尽量把其中环境依赖的部分剥离出来,对环境独立的部分单独测试,对环境依赖的部分用人工测试。然而在一个大的系统中,环境依赖部分太多,人工测试变得不可能,这时我们还是有必要认真做一下Mock这个工作的。

  当然还有其他的一些工作,包括对软件过程的制定,项目配置,都会随着单元测试的引入而需要做变动,但这不是我们这次讨论的重点,暂且忽略。

  说了这么多,总结一下:像显示效果这样的测试,靠单元测试是做不到的,只能用人工或者其他测试手段。但是如果你的系统是按照易于测试的原则实现的话,可以测试那些能够测试的部分,这样会给你的人工测试减轻很多压力,因为很多东西通过单元测试之后,人工测试就可以认为他们一定是正确的了。这就是我前一篇文章所提到的“分层测试”原则。

  另外,测试并不是功能较多良药,对任何系统都可以简单而方便的实施。为了做到易于测试,还是需要在前期(如需求阶段、编码阶段)做一番努力的。

0
相关文章