什么是单元测试

测试是软件开发的重要环节之一。按照软件开发的过程测试可分为:单元测试、集成测试、系统测试、域测试(Field test)等。

单元测试(unit testing)在软件开发领域有着悠久的历史。在大多数有关单元测试的观念中都有这么一个共同的理念,即它们由一组独立的测试构成,其中每个测试针对一个单独的软件组件。在过程式程序设计的代码中,“单元”一般来说指的就是函数,而在面向对象的代码中则指的是类。

那么,我们究竟能否做到只测试系统中的某一个函数或类呢?在过程式系统中,通常是难以孤立地测试函数的,因为这种系统中的情况往往是顶层的函数调用其他函数,后者再调用另一些函数,最后直到机器层。而在面向对象的系统中,单独测试类则要简单一点,然而实际上类却往往并不是“离群索居”的生物。想想看,在你写过的所有类中有多少是没有使用到别的类的?非常少这些极个别分子往往是那些小型的数据类或数据结构类,如栈和队列(甚至就算是这样的类也可能会用到其他类)。

测试的隔离性是单元测试的一个重要方面,然而为什么说它是重要的呢?毕竟,当整合软件的各个部分时还可能出现许多错误。难道不应该是那些能够覆盖代码中的广泛功能区域的大型测试更为重要吗?诚然,它们是重要的,我并不否认这一点,然而大型测试存在着一些问题:

q 错误定位:测试离被测试者越远,就越难确定测试失败究竟意味着什么。要想精确定位测试失败的根源往往需要耗费大量的工作。你得检查测试输入、还要检查失败本身,然后还得确定这次失败发生在从输入到输出的执行路径上的哪一点。虽说对于单元测试来说这样的工作也是免不了的,然而通常其工作量微乎其微。

q 执行时间:大型测试往往需要更长时间来运行。而这种长时耗性往往让人无法忍受。需要太长时间运行的测试,结果往往是无法运行。

q 覆盖:在大型测试中,往往难以看出某段代码与用来测试它的值之间的联系。我们通常可以通过覆盖工具来查出某段代码是否被一个测试覆盖到了,但当添加新的代码时,可能就需要花费可观的工作量来创建检验这段新代码的高层测试了。

而单元测试则做到了大型测试所不能做到的那些事情。利用单元测试可以独立地对某一段代码进行测试。我们可以将测试分组以便在某些特定条件下运行某些特定的测试,并在其他条件下运行另一些测试。我们还可以迅速定位错误。如果认为在某段代码中存在着一个错误而且又可以在测试用具中使用这段代码的话,我们通常能够迅速地编写出一段测试,看看我们所推测的错误是不是真的在那里。

测试的覆盖种类

1.语句覆盖:语句覆盖就是设计若干个测试用例,运行被测试程序,使得每一条可执行语句至少执行一次。

2.判定覆盖(也叫分支覆盖):设计若干个测试用例,运行所测程序,使程序中每个判断的取真分支和取假分支至少执行一次。

3.条件覆盖:设计足够的测试用例,运行所测程序,使程序中每个判断的每个条件的每个可能取值至少执行一次。

4.判定——条件覆盖:设计足够的测试用例,运行所测程序,使程序中每个判断的每个条件的每个可能取值至少执行一次,并且每个可能的判断结果也至少执行一次。

5.条件组合测试:设计足够的测试用例,运行所测程序,使程序中每个判断的所有条件取值组合至少执行一次。

6.路径测试:设计足够的测试用例,运行所测程序,要覆盖程序中所有可能的路径。

用例的设计方案主要的有下面几种:条件测试,基本路径测试,循环测试。通过上面的方法可以实现测试用例对程序的逻辑覆盖,和路径覆盖。

下面是好的单元测试所应具备的品质:

(1) 运行快;

(2) 能帮助我们定位问题所在。

在业界,人们在判断某个特定的测试是否是单元测试这个问题上常常摇摆不定。如果一个测试中涉及了另外一个产品类,那它还能算是单元测试吗?为了回答这个问题,我们回到刚才提到的两点品质上来,即该测试运行起来快不快?它能帮我们快速定位错误吗?比如有些测试较大,其中用到了好多类。那么实际上这种测试或许看上去像是小型的集成测试。就它们自身而言,可能运行起来比较快,然而要是你将它们一起运行呢?一个测试如果不仅测试了某个类还测试了与该类一起工作的几个类,那么它往往会“越长越大”。如果你当时不花时间来使得一个类能够在测试用具中单独实例化的话,难道你还能指望当更多的代码被添加进系统之后这件事会变得更容易吗?永远也不会。人们会不断推诿,并且随着时间的推移,原本短小的测试可能会变得需要十分之一秒才能执行完。

一个需要耗时十分之一秒才能执行完的单元测

这话是认真的。十分之一秒对于单元测试来说简直就像一个世纪一样。不信的话让我们来做一点简单的算术吧:假设你有一个项目,其中包含3 000个类,每个类平均大约有10个测试,一共算起来就是30 000个测试。倘若这些测试个个都耗时十分之一秒才能运行完的话,那整个项目测试一遍需要多少时间呢?将近一个小时!对于反馈来说这段等待时间可不短。什么?你的项目没有3 000个类?那一半总有吧,这样算下来也仍然要等半个小时呢。另一方面,假如我们的测试只需耗时百分之一秒呢?很显然,我们一下子从需要等一个小时变成了只需等5到10分钟!这样的话,虽说我还是比较谨慎的只取出其中的部分单元测试来用,但哪怕每隔几个小时就将它们全部运行一遍我也不再害怕。

单元测试运行得快。运行得不快的不是单元测试。

有些测试容易跟单元测试混淆起来。譬如下面这些测试就不是单元测试:

(1) 跟数据库有交互;

(2) 进行了网络间通信;

(3) 调用了文件系统;

(4) 需要你对环境作特定的准备(如编辑配置文件)才能运行的。

当然,这并不是说这些测试就是坏的。编写它们常常也是有价值的,而且你通常也会在单元测试用具内来编写它们。然而,将它们跟真正的单元测试区分开来还是很有必要的,因为这样你就能够知道哪些测试是你可以(在你进行代码修改的时候)快速运行的。

单元测试有下面的这些优点:

1、它是一种验证行为。

程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支缓。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。

2、它是一种设计行为。

编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。

3、它是一种编写文档的行为。

单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。

4、它具有回归性。

自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。

单元测试的范畴

如果要给单元测试定义一个明确的范畴,指出哪些功能是属于单元测试,这似乎很难。但下面讨论的四个问题,基本上可以说明单元测试的范畴,单元测试所要做的工作。

1、 它的行为和我期望的一致吗?

这是单元测试最根本的目的,我们就是用单元测试的代码来证明它所做的就是我们所期望的。

2、 它的行为一直和我期望的一致吗?

编写单元测试,如果只测试代码的一条正确路径,让它正确走一遍,并不算是真正的完成。软件开发是一个项复杂的工程,在测试某段代码的行为是否和你的期望一致时,你需要确认:在任何情况下,这段代码是否都和你的期望一致;譬如参数很可疑、硬盘没有剩余空间、缓冲区溢出、网络掉线的时候。

3、 我可以依赖单元测试吗?

不能依赖的代码是没有多大用处的。既然单元测试是用来保证代码的正确性,那么单元测试也一定要值得依赖。

4、 单元测试说明我的意图了吗?

单元测试能够帮我们充分了解代码的用法,从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。