在过去的几年里,单元测试已成为我编写软件的核心环节,多亏了一种称为极限编程 (XP) 的简便编程方法(请参阅参考资源)。这种方法要求我为添加的每个函数编写单元测试,并且要维护这些测试。如果单元测试失败,我就无法整合任何代码。随着代码库的不断增大,这些测试将使开发人员能够很有把握地完成更改。
起初,我认为有了单元测试,就没必要再进行功能测试。噢,又错了。功能测试与单元测试相差甚远。我花了很长一段时间理解二者的区别,以及如何结合使用两者来改进开发过程。
本文探究单元测试与功能测试之间的区别。并概述了在日常开发中使用这两种测试的方法。
测试对于开发人员极为重要,您必须在开发过程中不断进行测试。测试不应该只属于开发周期的某个特定阶段。它绝不应该是您将系统交给客户前要完成的最后一项任务。如何才能知道您何时就完成了所有任务呢?如何才能知道对一个小错误的修正是否破坏了系统的主要功能呢?目前想像中的系统如何才能演化为实实在在的系统呢?单元测试和功能测试都应该是开发过程中不可分割的一部分。
单元测试应成为您编写代码的核心环节,当您所做的项目时限很紧并且您希望控制开发进度时尤其如此。由于单元测试是如此重要,所以您应该先编写测试,再编写代码。
一套适当的单元测试具有以下功能:
单元测试创建随系统自然发展的设计文档。再读一遍上一句话。文档随系统自然发展,这是软件开发的“圣杯”。有什么方法比通过提供一个用例编码集来记录一个类效果更好呢?那就是单元测试:一系列记录类所做工作的用例代码,提供输出控制。这样,由于单元测试必须通过,所以设计文档总是最新的。
您应该首先编写测试,然后再编写代码。这样就为要测试的类提供了一种设计,这种设计使您每一时刻都只需集中考虑一小块代码。这种做法也使设计变得不再复杂。您没有试图为以后着想而实现一些不必要的功能。先编写测试还使您知道该类何时完成。一旦通过所有测试,任务也就完成了。
最后,单元测试可使您高度自信,这又会转化为开发人员的满意度。如果只要更改代码即运行单元测试,您立即就能发现您所做的更改是否对系统造成了破坏。
功能测试比单元测试更重要,因为功能测试将验证系统是否可以发行了。功能测试以一种有用的方式对您的工作系统进行说明。
一套适当的功能测试具有以下功能:
功能测试以有效方式捕获用户需求。传统开发通过用例来捕获需求。通常,人们讨论用例并花很长时间对它们进行细化。他们最后所得到的只是一纸空文。功能测试就像自验证式用例。极限编程方法可解释这一概念。XP Stories 将成为未来用户与开发人员进行沟通的协议。功能测试便是这种沟通的结果。未经功能测试的 Stories 不可能很完善。
功能测试填补单元测试留下的空白,并可增强小组对代码的信心。单元测试漏掉许多错误。尽管它可以提供您所需的全部代码,但它可能无法提供您所需的全部系统功能。功能测试将暴露单元测试遗漏的问题。一套适当的自动化功能测试也不可能捕捉到每个错误,但是它能比最好的单一单元测试捕捉更多的错误。
单元测试向开发人员表明代码正确执行操作;而功能测试向开发人员表明代码执行正确的操作。
单元测试
单元测试是从程序员的角度编写的。它确保类的某个特定方法成功执行一系列特定的任务。每个测试都确保只要给定输入,方法将输出预期的结果。
如果没有测试框架,编写一套可维护的自动化单元测试几乎是不可能的。在开始编写测试之前,请选择一个小组公认的框架。您将经常性地使用这个框架,因此您最好对它有点好感。极限编程网站提供了几个单元测试框架(请参阅参考资源)。我最熟悉的框架是 JUnit,它专门用来测试 Java 代码。
功能测试
功能测试是从用户的角度编写的。这种测试确保系统执行用户期望它执行的工作。
很多时候,系统开发好比建筑房屋。尽管这种类比不很恰当,但为了理解单元测试与功能测试的区别,我们可以扩充这种类比。单元测试好比房屋建筑现场的建筑监理员。他关心房屋的各个内部系统,如地基、构架、供电系统和管道设备等。他确保(测试)房屋每一部分的工作都安全、正常,即符合建筑说明。这种情况下,功能测试类似于视察同一建筑现场的房主。他假定内部系统将正常运作,并假定建筑监理员在执行其任务。房主关心的是住在这所房子里将会怎样。他关心房子的外观如何,各个房间的大小是否合适,房子能否满足家庭的需要,以及窗户的位置是否有利于采光。房主对房子执行功能测试。他从用户的角度考虑问题。建筑监理员对房子执行单元测试。他从建筑工人的角度考虑问题。
就像单元测试一样,如果没有测试框架,编写一套可维护的自动化功能测试实际上是不可能的。JUnit 非常适合编写单元测试;但是,当试图编写功能测试时,它就显得力不从心了。就功能测试而言,没有与 JUnit 相当的框架。也有几种用于功能测试的产品,但我从来没见过它们应用于生产环境。如果找不到满足您的需要的框架,您就必须创建一个。
无论我们多么擅长于构建手头的项目,也不管我们正在创建的系统多么灵活,如果我们的产品不合用,那我们就是白费时间。因此,功能测试是开发最重要的部分。
由于两种测试都必不可少,您就需要了解编写它们应遵循的原则。
刚开始编写单元测试时很容易恢心。最佳的入手方式就是为新代码创建单元测试。(尽管为现有代码创建单元测试比较困难,但并非无法实现)。首先从新代码着手,待您习惯了整个过程以后,再针对现有代码创建测试程序。
如上文所述,应该首先编写单元测试,然后再编写这些单元测试要测试的代码。如何为尚不存在的代码编写测试呢?问得非常好。掌握这一方法需要 90% 的思维加 10% 的技术。我的意思是,您只需假定您正在为其编写测试的类已经存在。接下来的任务就是编写测试。起初会犯很多语法错误,但您先别管它。这一步您要做的就是定义该类要实现的接口。下一步就是运行您的单元测试,修正语法错误(即,编写一个类,使它实现您的测试刚定义的接口),并再次运行测试。重复这一过程,每次仅编写修正故障的代码。运行测试,直到测试全部通过为止。一旦通过全部单元测试,代码也就完成了。
一般而言,类的每个公共方法都应有一个单元测试。但是,功能简单的方法(例如,getter 方法和 setter 方法)不需要单元测试,除非它们以某种特别的方式进行获取和设置。应该遵循下面这条很好的原则:即只要您认为有必要对代码中的某个行为加注,就编写一个单元测试。如果您像其他许多程序员一样不喜欢为代码加注,则单元测试是记录代码行为的一种方法。
将单元测试与被测试的相关类放在同一个包内。这种组织方式使每个单元测试都能访问被测试类中带有 package 或 protected 访问修饰符的方法和引用变量。
在单元测试中避免使用域对象。域对象是特定于某个应用程序的对象。例如,一个电子表格应用程序可能包含一个注册对象;这个注册对象就是一个域对象。如果您有一个已知这些域对象的类,则在测试中完全可以使用这些对象。但是如果您有一个根本不使用这些域对象的类,在测试中就不要将这些对象联系到该类上。应该避免这种情形完全是因为代码重用。为某一项目创建的类经常要用于其他项目。重用这些类可能很简单。但是,如果对重用类的测试中用到了另一个项目的域对象,则使测试能够正常运行这一工作就会相当耗时。通常情况下,这个测试将被删除或重写。
这些机制为您提供很好的帮助,但是如果您不运行这些测试,一套综合的单元测试就变得一文不值。尽早运行测试通常使您在任何时候都对代码充满信心。您将随着项目进展不断添加功能。运行这些测试将会通知您刚刚实现的新功能是否对系统造成了破坏。
在您掌握了编写单元测试的技巧之后,我们再来看看现有代码。为现有代码编写测试可能是个挑战。不要为测试而测试。当您发现有必要对一个未经很好测试(或者根本就没有测试)的类进行修改时,请“随时”编写测试。现在是添加测试的时候了。像往常那样,该类的单元测试应该捕获其每个方法的功能。找出应该进行哪些测试的最容易的方法之一是:查看现有代码中的注释。任何注释都应在单元测试内捕获。将位于方法开头、说明该方法所起作用的注释块翻译为单元测试。
尽管功能测试很重要,但它却没有受到足够的重视。多数项目都有单独的一个组来做功能测试。通常有一大群人不断地与系统交互,以确定系统是否正确工作。这种观念和设置专门的功能测试小组的做法很不明智。
对功能测试的处理与对单元测试的处理不应该有太大的区别。只要您编写的代码用来产生要求用户与之交互的组件(如对话框),就要编写测试,但实际上编写测试要在编写代码之前进行。请与用户一起编写获取用户需求的功能测试。无论何时开始一项新任务,都要在功能测试框架中描述此任务。您的开发工作将继续向前发展,当添加新代码时,请执行单元测试。当所有的单元测试都结束以后,运行最初的功能测试,看看它是否能够通过,或者是否需要修改。
从理论上讲,功能测试小组的概念该消失了。开发人员应与用户共同编写功能测试。在对系统所做的一系列功能测试结束之后,开发组中负责功能测试的成员就应该用初始测试的各种变化形式来轰击系统。
通常单元测试与功能测试之间并没有明确的界限。老实说,有时我也不清楚这个界限在什么位置。在编写单元测试时,我根据以下原则来确定当前编写的单元测试实际上是否是功能测试:
请注意“它可能是功能测试”这一措辞。本文无法提供硬性而快速的规则。单元测试与功能测试中之间有一个界限,但界限的具体位置要由您来确定。您用单元测试用得越熟练,某个特定测试是单元测试还是功能测试的界限就越明显。
单元测试是从开发人员的角度出发编写的,并且关注的是所测试的类的特定方法。当编写单元测试时,请使用以下这些原则: