3.1 单元测试
单元测试
单元测试(Unit Testing)又称“模块测试”,指对软件中的最小可测试单元进行检查和验证。一般来说,要根据实际情况判定单元的具体含义。例如,在C语言中单元指一个函数或子过程。在C++和Java这样面向对象的语言中指类或类的方法,在图形化软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。
单元测试是在软件开发过程中要进行的最低级别的测试活动,也是非常重要的测试。软件的独立单元将在与其他部分相隔离的情况下进行测试,目的是检验其能否正确地实现应有的功能,并且满足性能和其他条件下的要求。
3.1.1 作用
在软件开发的整个流程中前期会有静态测试手段,后期也会有集成测试和系统测试,进行单元测试的原因如下。
(1)在一个软件的开发过程中,当开发人员完成一个模块之后往往迫切希望进行软件的集成,以看到整个软件的工作情况。但是如果不进行单元测试,那么在后期的集成测试中将会遇到更多的缺陷。如此一来,不但不会节省时间,反而增加了工作量。因此在每一个模块编写完成后对其进行单元测试能够纠正更多的缺陷,从而为后期软件开发奠定良好的基础,使后期的集成测试工作变得更加高效和可靠。
(2)在软件开发中开发人员经常会忽略针对单元模块做出详细的规格说明,而是直接编码开发。所以当完成编码之后仅能从代码中找到该单元模块做了什么,而在做测试的时候能够表明的就是编译器正常工作,如此一来仅能发现编译器的缺陷。在实践过程中真正要理解的是该单元模块的功能,那么可以根据规格说明书进行代码的复查。以确保该单元模块没有错误,并进一步发现更多的缺陷,这是提高代码质量及降低开发成本的必由之路。
(3)如果软件有缺陷,则运行一次全部单元测试,找到未通过的测试单元可以很快地定位有缺陷的执行代码。修复代码后运行对应的单元测试,如果还不通过,则继续修改并再测试,直到测试通过。单元测试可以大大减少调试时间,从而达到节约时间成本的效果。
(4)规模越大的代码集成意味着复杂性越高,如果软件的单元没有事先测试,开发人员很可能会花费大量的时间仅仅是为了使软件能够运行,而任何实际的测试方案都无法执行。一旦软件可以运行,开发人员又要面对这样的问题,即在考虑软件全局复杂性的前提下对每个单元进行全面的测试。这是一件非常困难的事情,甚至在创造一种单元调用的测试条件的时候要全面考虑单元被调用时的各种入口参数。在软件集成阶段,对单元功能全面测试的复杂程度远远超过独立进行的单元测试过程,最后的结果是测试将无法达到它所应该有的全面性。一些缺陷将被遗漏,并且很多缺陷将被忽略。
(5)研究成果表明,无论什么时候做出修改都需要进行完整的回归测试,在软件生命周期中尽早地进行测试将使软件效率和质量都得到最好的保证。缺陷发现的越晚,修改它所需的费用就越高,因此从经济角度来看应该尽可能早地查找和修改缺陷。在修改费用变得过高之前,单元测试是一个在早期抓住缺陷的机会。相比后阶段的测试,单元测试的创建更简单,维护更容易并且可以更方便地重复。从全程的费用来考虑,相比那些复杂且旷日持久的集成测试,或不稳定的软件系统来说,单元测试所需的费用是很低的。
3.1.2 内容
单元测试的内容主要包括模块接口测试、路径测试、边界条件测试、错误处理测试等,还要包括语法检查、逻辑检查及局部数据结构测试等。
1.模块接口测试
模块接口测试是单元测试的第1步,包括以下6个方面。
(1)调用其他模块时,调用者所给的输入参数与模块的形式参数在个数、属性、顺序上是否匹配。
(2)输入的实际参数在个数、属性及类型上是否匹配。
(3)各个模块对全局变量的定义是否一致。
(4)文件在使用前是否已经打开。
(5)是否处理了输入输出错误。
(6)输出信息中的文字信息是否正确无误。
2.路径测试
造成路径错误的主要因素如下。
(1)运算优先次序不正确或误解了运算优先次序。
(2)运算方式错误,运算的对象彼此在类型上不相容、算法错误、初始化错误、运算精度不够,表达式的符号不正确等。
(2)循环次数不正确。
(3)错误或不可能达成的循环终止条件。
(4)不同类型数据的比较。
(5)不适当地修改循环变量。
3.边界条件测试
软件经常在边界上失效,采用边界值分析技术针对边界值及其临近值设计测试用例很有可能会发现错误,常见的边界值如下。
(1)数组元素的第1个和最后一个。
(2)循环的第0次、第1次、倒数第2次和最后一次。
测试所包含的边界检验的类型为数字、字符、位置、大小、方位、尺寸、空间等。
4.错误处理测试
比较完善的模块设计要求能预见出错的条件,并设置相应的出错处理,以便在软件出错时能重做安排,从而保证其逻辑的正确性。这种出错处理也应当是模块功能的一部分。
表明出错处理模块有错误或缺陷的情况如下。
(1)出错的描述难以理解。
(2)出错的描述不足以对错误定位,从而确定出错的原因。
(3)显示的错误与实际的错误不符。
(4)对错误条件的处理不正确。
(5)异常处理不当。
5.局部数据结构测试
模块的局部数据结构往往是错误产生的来源,常见的错误有如下几类。
(1)不适合或者不相容的类型说明。
(2)变量未初始化。
(3)使用未赋值的变量。
(4)出现溢出或地址异常。
(5)变量名不正确。
(6)数据类型不一致。
6.软件语法检查
软件语法检查可以从两个方面进行,即通过编译语言检查和人工检查。编译语言一般只检查语法的正确与否,不能检查软件的逻辑结构和功能性的错误;人工检查是一种静态的方法,可以检查软件的逻辑结构、处理的功能及书写的格式。软件逻辑检查主要检查软件中使用的逻辑是否合理、循环次数是否有问题,以及是否出现循环函数或子模块出现自我调用的问题。
经常与单元测试联系起来的另外一些开发活动包括代码复查(Code Review)、静态分析(Static Analysis)和动态分析(Dynamic Analysis)。静态分析研读软件的源代码,以查找错误或收集一些度量数据,并不需要对代码进行编译和执行;动态分析通过观察软件运行时的操作提供执行跟踪、时间分析,以及测试覆盖度方面的信息。
总之,单元测试应该以简单、易于调试、可靠、快速执行的方式来操作,借此检查软件的最小构建模块在被组合前是否能够正常工作。值得注意的是,虽然单元测试可以证明模块独立工作时是正常的,但是无法证明模块组合在一起后是否也能正常工作。
3.1.3 案例
【案例】对求绝对值abs()函数进行单元测试。
测试用例可以有如下选择。
(1)输入正数,如1、1.2、0.99,期待返回值与输入相同。
(2)输入负数,如-1、-1.2、-0.99,期待返回值与输入值的符号相反。
(3)输入0,期待返回0。
(4)输入非数值类型,如None、[]、{},期待提示“Type Error”。
把上面的测试用例放到一个测试模块中就是一个完整的单元测试。
如果单元测试通过,说明被测函数能够正常工作;否则要么函数有缺陷,要么测试条件输入不正确。总之,需要修复使单元测试能够通过。
如果我们修改了abs()函数代码,只需要再执行一遍单元测试。如果通过,说明修改不会对该函数原有的行为造成影响;如果测试不通过,说明修改与原有行为不一致,只能修改代码并重新测试。