王云婷,刘 辉
(石家庄铁道大学信息科学与技术学院,河北 石家庄 050043)
随着计算机软件技术的发展,软件测试在软件生命周期中的作用越来越受到业界的重视。软件测试从发生的时间点来看,可分为单元测试、集成测试、系统测试和验收测试几个阶段[1]。在软件测试中,单元测试是最早开始的测试。实践证明,单元测试可以发现15.75%的错误[2]。然而手工测试繁琐,耗时耗力,自动化单元测试软件可提高测试效率,缩短开发周期。而且可以快速得到测试结果以供测试人员分析,还可以方便地进行回归测试。
笔者应用JUnit框架测试堆栈处理数学表达式的程序,探索了JUnit框架在单元测试中的应用策略。首先分析了JUnit测试框架中设计模式的应用,然后将JUnit测试框架应用于堆栈处理数学表达式的程序中,并对测试的过程进行了深入的探讨。
JUnit框架很好的实现了测试驱动的软件过程。使用JUnit框架进行测试的步骤为:
(1)生成测试用例,即继承TestCase类,编写测试用例。如果需要集中测试若干个测试用例,则继承TestSuite类,创建测试套件,将需要测试的测试用例逐个添加到测试套件中。
(2)执行测试。将测试用例或测试套件作为请求发送给JUnit框架。JUnit框架会将该请求发给负责收集、保存结果的类TestResult。该类的对象执行测试,记录测试过程中的错误、失败,反馈给测试人员。
(3)测试结果收集及断言。就是对JUnit生成的测试用例、测试套件以及结果进行收集,并判断结果与预期结果之间的差异,从而得出测试结论。
在JUnit中编写测试用例,测试人员继承TestCase类,编写测试内容,把测试用例作为请求参数,发送给JUnit框架。接下来由JUnit框架记录测试结果、执行时间、错误方法、错误位置,再返回给测试人员。
JUnit这种生成测试用例的方法借鉴了设计模式中的命令模式,即一个操作生成一个对象并为其提供一个执行方法。Test类是JUnit框架中一个抽象接口,是所有测试类必须实现的接口,其中的run()方法就相当于命令模式中的执行方法。TestCase类实现了Test接口的run()方法,用于将该测试用例自身对象作为参数,传给TestResult类对象处理。Test类与TestCase类的UML类图表示如图1所示。命令模式使得JUnit框架不需考虑测试用例内容,测试员不用关心框架内部结构,实现了测试用例与测试框架的解耦。
图1 JUnit测试框架
在实际开发中需要多个测试用例组合起来一起测试。通常的方法是把几个测试用例组合成测试套件,生成请求发送给JUnit框架。JUnit框架分析请求,判断它是单个的测试用例还是测试套件,如果是测试用例就直接执行run()方法,如果是测试套件,就进行逻辑分析。也就是说,JUnit必须区分单个测试用例和测试套件。在JUnit中采用组合模式进行实现,使测试框架用一样的方法处理测试用例和测试套件。
在图1中,TestCase类和TestSuite类都实现了Test接口和它的run()方法。同时测试套件类TestSuite还包含添加测试用例和添加测试套件的方法,属性fTests用于保存子测试用例。如此一来,所有测试套件就像一棵树(所以组合模式也称树模式),TestSuite表示还有子节点的分支节点,TestCase为叶子节点。这样一来,一组TestCase可以被集中的测试,而TestCase在树中的地位跟TestSuite等同,简化了处理方式,不再需要繁琐的分支判断。
1.3.1 收集
测试的目的是得到测试结果,从而发现程序中的错误,进而改进程序。JUnit 用TestResult类收集测试结果。如何收集结果也是JUnit框架需要解决的问题。在JUnit框架中采用参数收集模式进行解决。如图1所示,TestCase为每一个待测试方法的run()方法重载一个run(TestResult tr)方法,使用这个方法通知TestResult测试开始,并在异常处理块中向负责本方法的TestResult对象添加错误或失败的信息。
1.3.2 断言
断言用于判断测试结果与期望结果是否相符,或差值是否在能接受的范围内。JUnit区分了失败和错误,失败是可预期的结果,而错误是不可预期的,比如数组越界、类型转换失败等,即错误一定是代码漏洞,但失败不一定是。在JUnit中提供了丰富的断言函数,典型的如:
Public static void assertEquals(String message,double expected,double actual,double delta)
其中expected表示期望值,actual为观察值,而delta为可容忍的偏差。如果期望值与观察值的偏差大于delta,则抛出异常,添加到相应的TestResult对象中。大部分断言函数被重写,用于判断各种类型的数据,包括泛型类。
本文测试环境在操作系统为64 位windows7 旗舰版下搭建,使用JUnit4,及MyEclipse10.0。测试实例使用后缀表达式生成算法实现代码。
后缀表达式生成算法实例代码中包括三个方法,声明如下:
(1)public ArrayList<String>getArray(String text):用于将输入的表达式转换为ArrayList对象的内容。
(2)public String getPostfix(String editText):输入中缀表达式,获取其后缀表达式。其程序流程如图2所示。
(3)public String countString(String text):通过输入的中缀表达式,计算结果。
从图2可看出,程序对每个元素进行判断。如果是数字或小数点,直接加入结果集(后缀表达式),如果是左括号,将其入栈。若是右括号则读取栈顶元素,栈顶元素为左括号,就将左括号出栈,读下一个元素,若不为左括号则出栈并加入结果集,继续读栈顶元素。若读取的元素为操作符(即+,-,*,/),看其优先级是否高于栈顶元素(“(”最高,“)”最低),若是,则直接入栈,若不是,则进行出栈操作并将出栈元素加入到结果集中。
实例在MyEclipse环境中分别对这三个方法进行单元测试,根据预设的输入数据和预测数据验证被测方法的正确性,以及JUnit框架的有效性。然后将对这三个方法进行套件测试,探讨JUnit套件测试的可用性。
图2 被测代码流程
从源码的角度分析,JUnit框架进行测试时分为以下几个阶段。
首先,由TestRunner(JUnit中的测试运行器)为TestCase创建TestSuite。TestCase类中有suite()方法,TestRunner调用suite()方法,将TestCase实例加入。如果在创建的TestCase中没有该方法,则创建默认的测试套件将TestCase实例加入。
随后由TestRunner创建TestResult实例,用于收集测试结果。TestResult创建过程的时序图表示如图3所示。
(1)TestRunner创建一个TestResult对象,在测试运行期间,这个对象用于保存测试结果。
(2)TestRunner向TestResult注册,这样,TestResult发出消息(测试开始、失败、错误、测试结束等),TestRunner接收到消息后,显示实例中的测试进度条,它也可以在测试运行期间获得已经测试完成项目的结果,而不必等到所有的测试结束再得到结果。
(3)TestRunner调用run(TestResult)方法,将所注册的TestResult对象传给TestSuite对象。
(4)由TestSuite对象依次执行测试套件中的测试用例,并把TestResult对象传给要执行的测试用例。
(5)TestCase接收到TestResult实例,调用该实例的run(Test)方法,其形参就是该TestCase实例。
(6)至此JUnit框架将控制权交给了TestResult,TestResult对象通知所有注册时的对象测试开始。
(7)TestResult对象调用runBare()方法,执行测试用例。
TestResult调用runBare()方法,runBare()方法执行时,会先后执行由@Before、@Test、@After注解的方法。在这个过程中如果出现错误、失败等问题,TestResult会调用相应的方法,记录、保存错误和方法,并通知TestRunner。TestRunner会列出这些错误和失败,如果没有,则进度条是绿的。当@After的方法执行完后,TestResult会通知TestRunner测试完毕。
图3 TestResult创建时序图
(1)创建测试用例:即创建一个测试类,该类继承TestCase类。在JUnit4中测试方法需用@Test注解,使JUnit框架识别被测试的方法,该方法被public修饰,返回值为空。测试人员也编写@Before注解的方法,写一些测试执行之前需要进行的初始化工作,在@After注解的方法中编写测试完成后需进行的操作。
(2)编写测试代码:以getPostfix()为例编写单元测试的代码,部分代码如下:
为了方便测试,这里采用JUnit的参数化测试运行器进行测试。在JUnit中测试运行器(Runner)用于控制测试类运行的方法,参数化测试运行器是JUnit提供的用于测试多组数据的运行器。该运行器得到TestCase类对象和公共静态类中的参数,用所提供的参数初始化测试类中的属性,实例化测试类,即输入值(input)和期望值(expected),所以测试方法中用到的这两个属性就是prepareData()方法中提供的值[3]。
(3)执行测试:在prepareData()方法中提供了两组测试数据,可以看出第二组测试数据输入值的类型不是字符串,所以本测试应该有一个通过,一个失败(参数类型不匹配)。测试效果如图4(b)所示。从测试结果可以看出JUnit4框架不但记录每一个测试的结果、时间,而且对于失败的测试给出失败的代码位置及原因。
就第一个测试而言(输入数据为:3*4+(6-2)/2),根据图2的程序流程可得其逻辑覆盖路径:
A→L→B→D→G→L→A→L→B→D→H→D→G→L→A→L→B→D→G→L→A→L→B→L→G→L→A→L→B→C→E→C→F→L→B→D→G→L→A→I→K。覆盖了程序的所有路径,测试了程序的正确性。
(1)创建套件测试
由于采用组合模式,JUnit框架中测试用例与测试套件的地位相同,所以创建测试套件的方法比较灵活。可以实例化一个TestSuite类,调用其中的addTest()方法。也可以利用JUnit4内置的专门用于运行多个测试用例或测试套件的测试运行器(Runner),即Suite,将测试类用@RunWith(Suite.class)注解即可。这里介绍第二种方法在MyEclipse中的应用。
首先创建countString()和getPostfix()的套件测试案例。方法为,新建JUnit TestSuite,选择需要的两个测试类,点击Finish即可生成TestSuite实例,直接运行即可执行测试。生成的TestSuite的代码如下:
在此,新建的测试套件命名为CountAndPostfix,将测试套件CountAndPostfix与测试用例StackArrayTest组成测试套件的方法与将两个单元测试用例的方法是一样的。整个测试套件的结构表示如图4(a)所示的树一样,单元测试用例为叶子节点,测试套件为分支节点。
图4 测试套件
(2)执行测试
测试结果如图4(b),同样以树的形式清晰明了的展示了各个测试用例和测试套件之间的关系。
在对后缀表达式算法的实现程序进行白盒测试过程中,用JUnit框架编写测试用例,实现了测试用例的复用,多个测试用例的集合测试,测试结果(包括测试时间、失败和错误原因等)的详细汇报,提高了测试效率。
从算法实现的角度分析了JUnit框架的应用。在当前应用中,图形用户界面(GUI)所占比例较大。GUI软件的各界面可能有多个事件,事件之间的组合随着事件数的增加以指数形式增长,考虑不周会增大软件缺陷出现的概率,穷举测试不现实,因此合理生成测试用例就变得极其复杂[4]。JUnit在GUI测试方面的应用将会是后续工作中的重点研究内容。
[1]Galler S J,Aichernig B K.Survey on test data generation tools[J].International Journal on Software Tools for Technology Transfer,2013:1-25.
[2]Nguyen B N,Robbins B,Banerjee I,et al.GUITAR:an innovative tool for automated testing of GUI-driven software[J].Automated Software Engineering,2013:1-41.
[3]黄恩博,黄耿生,林延庆.软件测试学研究[J].福建电脑,2012(12):58-60.
[4]杨学红.自动化单元测试概述[J].信息通信技术,2012(1):66-68.