面向对象自动化单元测试技术研究
2020-06-28刘芳
刘芳
(四川大学计算机学院,成都 610065)
0 引言
软件测试是软件开发过程中的一个重要阶段,因为测试可以在开发过程中发现缺陷并避免缺陷出现在最终产品中。早期对测试的定义更多是将测试看做对软件的检验——检查软件的功能是否正常运行,正如1983 年Bill Hetzel 博士对软件测试的定义:软件测试是一系列活动,这些活动是为了评估一个程序或软件系统的特性或能力,并确定它是否达到了预期的结果。同时,IEEE 提出的软件工程标准术语中[1],对软件测试的要求是通过软件测试检查软件是否满足设计阶段提到的所有需求、检查软件是否可以为不同的输入提供正确的输出、检查软件是否能够在限定时间内或可接受的时间内完成任务、检查软件是否可以在不同的环境中运行。
软件测试是评估软件以确定其质量的重要手段,然而测试通常会消耗40%到50%的开发工作,并且当前的一些工业实践基于密集的测试,例如持续集成、交付、部署,因此为了降低开发成本和提供更好的软件质量,软件测试最好采用尽可能高的自动化级别,而不仅是测试用例自动化执行。自动化测试生成的想法是在20 世纪70 年代首次提出的,当时,由于机器缺乏足够的处理能力和内存,没有可用的工业解决方案,如今,众多自动化测试生成技术、几十种工具致力于解决这个问题,其中一些工具已经被软件开发人员用于实践。
单元测试是一种简单但有效的技术,可以提高软件的质量、灵活性和完成时间;单元测试的一个关键思想是每段代码都需要有自己的测试用例,而设计这些测试用例的最佳人选是软件开发人员。但是,手工为每个单元编写单元测试用例非常费时且成本昂贵,所以自动生成单元测试用例对于支持单元测试至关重要;并且,随着单元测试越来越受到关注,开发人员对自动生成单元测试用例的工具的需求也越来越大,但是由于目前自动化测试生成技术众多、工具众多,开发人员几乎没有关于这些技术、工具的有效信息,本文就面向对象单元测试、以及应用于面向对象单元测试自动化领域的流行技术进行研究讨论。
1 面向对象单元测试
近年来,面向对象软件开发方法已迅速成为开发大型系统首选的开发方法,其原因是众所周知的,首先,面向对象开发方法引入了对象、类的概念,类提供了一种优秀的结构机制,使得系统可以被划分为定义良好的单元,然后分别实现这些单元;其次,类支持信息隐藏,类可以导出纯粹的过程性接口以供调用,而内部数据结构是隐藏的,以此简化了维护;第三,面向对象鼓励并支持软件重用,这可以通过在库中重用类或通过继承实现,通过继承可以创建一个新类作为基类的扩展,在这两种情况下,结果都减少了代码编写量,并且由于可以利用以前测试过的类,因此可以提高系统的可靠性[2]。
传统的软件工程是面向过程的,即结构化软件开发方法,其基本思想是自顶向下,采用模块化技术分而治之,将系统按功能分解为若干个小模块进行分析和设计。基于结构化软件开发方法的特点,其单元测试的最小可测单位是程序的一个函数、过程或完成某一功能的程序块,通过输入、处理、输出三个步骤检验函数或过程是否正确[3]。
面向对象软件开发方法的主要思想是把系统的各个事务分解成各个对象,对象的概念不是一个步骤,而是描述一个事物在解决问题的过程中的行为,对象包含自己的数据(属性)和行为(方法),不同于传统的软件开发方法将数据分离,面向对象软件开发方法将数据和方法封装在一起,实现数据封装和信息隐藏。面向对象软件开发方法具有多种特性:对象、类、封装、多态、继承、聚合等,这些特性对于软件测试的影响也不尽相同[4]:
(1)将数据和功能(操作数据的方法)包装到对象中称为封装,封装隐藏了信息,限制了数据可见性,对应的也限制了软件测试中间结果的可见性,加大了测试难度。
(2)从旧类派生新类的机制称为继承,子类可以继承父类的属性和方法,并根据自身需要定义新的方法,继承减少了代码的冗余但增加了代码间的依赖性。由于继承关系,子类和父类可能拥有相同的方法,但使用语境不相同,所以对于子类和父类的方法都需要进行测试。
(3)多态是指同一行为具有不同的表现形式,多态种最为常见的是重载,重载就是方法名相同,但参数类型却不同。对于这个特性,测试需要根据相应的数据信息来选择对应的实现代码,需要测试所有可能的结合,增加了测试路径。
面向对象软件开发中,其单元测试的最小可测单位是封装的类或对象,对类进行测试时,类中的新方法、继承的方法和重新定义的方法都需要进行测试,对方法的测试需要确保语句覆盖(确保所有语句至少被遍历一次)、条件覆盖(确保所有条件执行)、路径覆盖(确保执行的是循环真/假部分)。使用以下方法进行单元测试:测试类中的每个方法和构造函数;测试方法之间的类的状态行为(属性)。
2 面向对象自动化单元测试技术
软件测试被广泛认为是软件开发过程中的关键部分,尤其是在持续集成的背景下,开发人员针对新的代码更改运行单元测试和集成测试以迅速发现存在的缺陷,然而编写好的测试,尤其是编写好的单元测试代表了最困难、最耗时的测试活动之一。因此,学术界和工业界都投入了越来越多的精力来实现自动生成单元测试用例的方法和工具[5]。在面向对象语言中,每个单元测试都是一系列方法的调用,在这种情况下,用于自动测试生成的流行技术包括基于随机的测试生成技术、基于搜索的测试生成技术和基于符号执行的测试生成技术。
2.1 基于随机的测试生成技术
随机测试(Random testing)是最基本、最流行的单元测试生成技术之一,基于随机的测试生成技术基本上是由随机生成器和方法调用序列驱动的,随机生成器可以提供格式良好但随机的数据作为测试输入,随机测试的随机性可以体现在以下方面:测试数据的随机性、方法调用的随机性、方法调用顺序的随机性。在2002 年的软件工程百科全书中,D. Hamlet[6]对随机测试进行了详细的描述,并用一个简单的例子说明了随机测试:一段程序用来计算整数参数的立方根,参数的区间为X=[1 ,107] ,假设参数在该区间的所有取值具有相同的可能性,为该程序执行3000 点随机测试的方法是:生成3000 个满足参数区间且均匀分布的伪随机整数x,执行程序并为其传入参数(生成的伪随机整数),得到计算结果zx,用与x 进行比较,判断程序是否实现了为输入参数计算立方根的功能。
在面向对象软件中,测试用例通常由一系列方法调用组成,每个步骤调用一个对象o 的m 方法,表示为o.m(T1v1,T2v2,T3v3,...,Tnvn),其中调用方法传入的Tivi是对应参数类型的值。生成一个随机测试用例,首先是创建对象o 使它可以接受方法m 的调用,其次构建Tivi类型的值作为调用方法m 的参数,对方法m 的成功调用将返回一个值r,该值可在后序测试中用作其他方法的参数,如果调用过程中出现异常,则认为方法m 没有通过测试[7]。
随机测试生成的主要优点是它可以在很短的时间内达到较高的覆盖率,并且随机生成的测试用例可以用于回归测试。然而,该技术在寻找软件中的复杂缺陷和覆盖复杂条件下的路径方面并不理想,所以从随机测试的理论基础上衍生出了一项新技术——自适应随机测试(Adaptive Random Testing),他允许测试人员通过不同的因素来控制测试用例的生成,例如指定一组支付穿,测试生成器可以从中创建有效的SQL 请求,从而增强了随机测试。反馈导向的随机测试生成技术(Feedback-Directed Random Test Generation)[8]也是对随机测试的一种改进,该技术结合了从开始执行测试输入获得的反馈来改善随机测试的生成,反馈导向的随机测试生成技术通过随机选择方法进行调用,并从先前构造的输入中查找自变量来逐步构建输入,构建输入后,将立即执行该输入并根据一组规范和过滤器进行检查,执行的结果确定生成的测试输入数据是否冗余、非法、违反合同或对生成更多的输入有用。
2.2 基于搜索的测试生成技术
自1992 年以来,基于搜索的优化技术已广泛应用于软件测试数据的生成,该技术称为基于搜索的测试,基于搜索的测试是使用基于搜索的优化算法,在适应度函数的指导下,根据测试充分性标准自动生成测试数据的过程。适应度函数的作用是捕获一个测试目标,当实现该目标时,将有助于实现所需的测试充分性标准,使用适应度函数作为指导,搜索将寻找能够最大程度实现测试目标的测试输入。因为可以定义不同的适应度函数来捕获不同的测试目标,从而允许将相同的基于搜索的整体优化策略应用于非常不同的测试数据生成场景[9]。
应用于软件测试数据生成中的搜索算法包括爬山算法、模拟退火算法和进化算法,其中使用最广泛的是进化算法中的遗传算法。遗传算法遵循解进化的概念,利用给定的适应度函数随机生成解种群,遗传算法应用于测试生成过程时,可以用来专门寻找覆盖程序某些分支的特殊测试场景。在遗传算法中,一个候选个体的种群(即测试用例),使用搜索运算符(交叉、变异)来模拟自然进化,例如两个个体之间的交叉产生两个包含双亲遗传物质的后代,或个体的变异引入新的遗传物质。适应度函数用于衡量个体相对于优化目标的好坏程度,适应度值越好,个体被选择繁殖的可能性就越高,从而逐步提高最佳个体在每一代中的适应度,直到找到最佳解决方案或保持一些其它的停止条件。适应度函数一次用一个输入执行被测程序,并测量该输入与选择作为优化目标的特定结构实体的距离。将遗传算法应用于测试数据生成中,算法如图1[10]:首先从一个随机总体开始进行进化,直到找到满足覆盖标准的解决方案,或者分配的资源(例如时间、适应性评估的数量)用完为止,在进化的每个迭代中,都会创建新一代并使用上一代最佳个体进行初始化,然后新一代被等级选择(第5 行)、交叉(第7 行)和变异(第10行)产生的个体所填充,根据适应性和长度限制,将子代或父代添加到新一代中。
图1 测试用例生成算法
2.3 基于符号执行的测试生成技术
符号执行是测试输入生成的一种良好的技术,尽管符号执行的概念诞生于20 世纪70 年代,但直到最近才在实践中用于测试生成,主要是因为符号执行需要大量的处理能力(或时间)和系统内存。符号执行通过符号变量而不是实际变量来运行被测类,以发现输入和输出之间的关系。该技术利用没有具体值的符号变量来收集被测类的路径条件,收集路径条件后,将其转化为正式问题(可满足性模块理论)并传递给求解程序以满足表达式,根据求解器的响应,可以确定哪些输入覆盖了代码的哪些路径或相应的行[11]。
符号方法将程序的执行路径表示为对输入值的约束,一种常见方法是动态符号执行,在这种方法中,系统地探索程序的路径,一次迭代否定路径约束中的一个分支条件,并使用约束求解器为该路径生成一个新的测试输入,大多数这类方法的目标是生成特定的输入数据,并且需要手工构建测试驱动程序。另一种是静态符号执行,在每个分支节点上更新路径条件,通过约束求解器判断路径是否可行,如果路径不可行,回溯到上一个节点,因此只执行可行路径。
3 面向对象自动化单元测试技术的应用
3.1 面向对象自动化单元测试工具
基于随机的测试用例生成技术开发的面向对象自动化单元测试工具包括JCrasher[17]、Jartege[18]、Randoop[19]、PEX[20]等,其中最为流行的是Randoop 工具,Randoop 是一个根据MIT 开放源码许可证发布的实验性工具,Randoop 可以生成用于回归的单元测试用例和揭示错误的单元测试用例。Randoop 的输入是一组要测试的Java 类、一个时间限制和一组可选的契约检查器,结果的输出是一个JUnit 测试套件。回归测试是不违反提供的契约,但捕获当前实现的测试,而违反契约的测试可能会揭示错误。Randoop 使用反馈定向随机测试生成单元测试,这种技术受到随机测试的启发,使用从创建测试输入时收集的执行反馈,以避免生成冗余和非法输入。该工具通过随机选择要调用的方法并从之前构造的序列中选择参数来创建方法调用序列,创建后,将执行一个新序列并根据一组契约进行检查。违反契约的序列作为揭示错误的测试输出给用户。显示正常行为的序列(没有异常或违反契约)作为回归测试输出。
基于搜索的测试用例生成技术开发的面向对象自动化单元测试工具包括Evosuite[21]、TestFul、eToc 等,其中使用最多且性能最好的是Evosuite 工具,Evosuite 应用遗传算法来生成一组可以最大程度覆盖代码的测试用例,它从一组随机测试用例的测试套件开始,然后迭代地应用搜索运算符(例如选择,变异和交叉)来进化它们,进化由基于覆盖标准的适应度函数指导,默认情况下是分支覆盖。搜索结束后,就覆盖标准而言,具有最高代码覆盖率的测试套件将被最小化,并添加回归测试断言,然后,Evosuite 通过编译来检查每个测试在语法上是否有效,并执行该测试以检查它是否稳定(即通过)。
基于符号执行的测试用例生成技术开发的面向对象自动化单元测试工具包括DSC[22]、CATG、PET[23]、Symbolic PathFinder(SPF)[24]等,其中最为流行的基于符号执行的测试工具是SPF,SPF 工具将符号执行与模型检查和约束求解相结合,用于在具有未指定输入的Java程序中自动生成测试用例和错误检测,在这个工具中,程序在代表多个具体输入的符号输入上执行,变量的值表示为Java 字节码分析产生的约束,使用现成的解决方案来生成保证实现复杂覆盖标准的测试输入,从而解决约束。
3.2 面向对象自动化单元测试工具的应用
为了克服手动编写单元测试用例的挑战:费时、成本昂贵、测试不全面等,自动化单元测试领域已经引入了基于不同方法的自动技术和工具,但是工业界对自动测试生成工具的采用有限。为了使开发人员能够采用这些工具,重要的是了解功能的能力和所生成的测试用例的质量,大量学术研究致力于自动化工具的性能研究、性能比较,研究工具在实际系统中的表现等。
A.Bacchelli 等人[12]在2008 年首次对手动单元测试和自动化单元测试进行了探索性研究,目的是从经验上理解手动和自动单元测试之间的差异,作者团队从代码覆盖率、变异分数以及缺陷检错率的角度证明了自动化单元测试工具(Randoop、Junit Factory、JCrasher)生成的测试用例的有效性。十年后D.Serra[13]团队采用A.Bacchelli 团队的研究方法,选取了三种最新的面向对象自动化单元测试工具(Evosuite、Randoop、JTExpert),研究表明当前的自动测试用例生成工具比手动编写的测试用例更好地优化了覆盖率和变异分数,但是最近十年的进步并不显著,在发现缺陷方面没有什么改进。
M.M.Almasi 等人[14]选取了SEB Life & Pension Holding AB Riga Branch 拥有的财务系统作为被测项目,研究工具在工业应用中的有效性和适用性,作者团队选取了Randoop 和Evosuite 两个工具作为研究对象,从被测系统的历史版本中提取了25 个实际缺陷,Randoop 的缺陷检测率为38%,Evosuite 的缺陷检测率为56.4%。
R.Ramler 等人[15]选取了Java 集合类库中的35 个实际缺陷,研究并比较了手动单元测试和随机测试用例生成的有效性,实验利用Randoop 工具生成测试用例,以及48 位硕士参与者进行手动单元测试,研究发现通过Randoop 生成的测试用例检测到的缺陷在手动单元测试范围内,而且工具检测到的缺陷与手动单元测试不同,Randoop 会检测到无法通过手动单元测试发现的缺陷。
L.Cseppento 等人[16]为了评估自动化单元测试工具的不同优势以及反馈工具的实际功能,作者团队收集了一组由工具处理的代表性编程语言概念(基本类型、运算符、数据结构、对象及其关系、泛型、内置类库等)将其映射到300 个代码片段,利用CATG、JPET、SPF、Evosuite、PEX 这五个工具为其生成测试用例,研究表明CATG 的基本功能没有问题,只是该工具不支持浮点数,无法生成数组作为测试输入,也无法解决数组索引的约束;JPET 工具不支持大多数内置Java 对象,尽管其支持浮点数,但它不支持复杂条件;SPF 支持除模运算之外的所有基本类型和运算符,并且仅存在最困难的条件语句和循环问题;Evosuite 是唯一能够完全覆盖对象和泛型的所有的代码段的工具;PEX 能够为所有的代码片段生成测试用例,该工具会检测到异常,并在时间限制到期后自行关闭。
4 结语
在软件开发过程中,软件测试是必不可少的一个环节,面向对象软件开发方法由于其特性已迅速成为开发大规模系统的首选开发方法,所以近年来关于面向对象自动化单元测试技术和工具的研究已非常成熟及多样化。本文对可用于测试面向对象的自动化单元测试用例生成技术,以及基于这些技术开发的工具进行了全面的概述。