在软件测试过程中,单元测试属于早期的测试活动,一般由开发人员完成,但测试人员也会参与。在单元测试活动中,强调软件单元的独立性,即在被测单元与程序的其他部分相隔离的情况下完成测试。
单元测试的重要性在于它是软件测试的基础,单元测试的效果直接影响到软件的后期测试,最终在很大程度上影响到产品的质量。越早发现缺陷,解决缺陷的成本就越低,而且在单元测试时,某些问题很容易被发现,如果将这些问题留到后期,测试的成本和风险将成倍上升。单元测试始终追求的目标是尽早地发现与解决问题,从而保证产品的质量。
JUnit测试框架
JUnit是一个开源的Java测试框架,用于构建可重复的单元测试,可以看作单元测试框架体系xUnit的一个实例,主要特性有:
1) 用于测试期望结果的断言;
2)用于共享共同测试数据的测试工具;
3) 用于方便地组织和运行测试的测试套件。
JUnit的源代码是公开的,可以针对特定需求进行二次开发,而且JUnit具有很强的扩展性,使之具有良好的适应性。除此之外,JUnit还具有下列优点:
4) 可以使测试代码与产品代码分开,有利于代码的打包发布和测试代码的管理;
5) 针对某一个类的测试代码,做较少的改动便可以应用于另一个类的测试,JUnit提供了一个编写测试类的框架,使测试代码的编写更加方便;
6) 易于集成到程序构建过程中,如通过和Ant的结合实现持续集成和持续测试。
JUnit一共有7个包,其核心的包是JUnit.framework和JUnit.runner。framework包负责整个测试对象的构建,runner负责测试驱动。JUnit还包括4个重要的类:TestSuite、TestCase、TestResult和TestRunner。另外,JUnit还包括Test和TestListener接口、Assert类。
1) Assert类用来验证条件是否成立,当条件成立时,assert方法保持沉默;若条件不成立时就抛出异常。Assert超类提供8个核心方法,见下表。
2) Test接口类用来测试和收集测试的结果,采用了Composite设计模式,它是单独的测试用例、聚合的测试模式及测试扩展的共同接口。
3) TestCase抽象类用来定义测试中的固定方法,TestCase是Test接口的抽象实现。由于TestCase是一个抽象类,因此不能被实例化,只能被继承。其构造函数可以根据输入的测试名称来创建一个测试用例,提供测试名称的目的在于方便测试失败时查找失败的测试用例。
4) TestSuite是由几个TestCase或其他的TestSuite构成的。基于TestSuite可轻松构建一个树形测试,每个测试都由持有另外一些测试的TestSuite来构成。加入到TestSuite中的测试在一个线程上依次被执行。
5) TestResult负责收集TestCase所执行的结果并将结果分类,分为客户可预测的错误和没有预测的错误。它还将测试结果转发给TestListener处理。
6) TestRunner是客户对象调用的起点,它跟踪整个测试过程,能够显示测试结果,并且报告测试的进度。
7) TestListenter对测试结果的处理和测试驱动过程的工作特征进行提取,包含4个方法:addError()、addFailuer()、startTest()和endTest()。
TestNG与JUnit比较
TestNG是一个不错的测试框架,尤其是在进行模块测试和大范围的测试时,它比JUnit更灵活。JUnit4推出后,其很多功能都与TestNG相似,但相对于JUnit4,TestNG还有较大的差异。
1. 灵活性
同JUnit4一样,TestNG同样支持Before、After方法,如同setUp和tearDown。不过, TestNG更灵活,支持各种签名方式,如private、protected等,其代码如下:
@BeforeMethod
protected void beforeMethod() {
System.out.println("in beforeMethod");
}
@AfterMethod
protected void afterMethod() {
System.out.println("in afterMethod");
}
TestNG同样也支持BeforeClass和AfterClass,只执行一次的方法,但是可以不须要使用static签名,其代码如下:
@BeforeClass
protected void beforeClassMethod() {
System.out.println("in beforeClassMethod");
}
@AfterClass
protected void afterClassMethod() {
System.out.println("in afterClassMethod");
}
JUnit框架想达到的一个目的就是测试隔离,导致人们很难确定测试用例执行的顺序,而这对于各种依赖性测试又非常重要。虽然人们想出了多种方法来解决这个问题,例如,按字母顺序指定测试用例,或者依靠fixture来解决问题。
与JUnit不同,TestNG利用“Test注释”的dependsOnMethods属性可以很容易地处理测试的依赖性问题。例如,testMethod2依赖于testMethod1,可以描述如下:
@Test
public void testMethod1() {
System.out.println("in testMethod1");
}
@Test(dependsOnMethods="testMethod1")
public void testMethod2() {
System.out.println("in testMethod2");
}
当然如果testMethod1失败,默认testMethod2也不会执行。不过,只须设置alwaysRun =true,则若testMethod1失败,可以跳过testMethod1,而执行testMethod2,描述如下:
@Test
public void testMethod1() {
System.out.println("in testMethod1");
throw new RuntimeException("failed");
}
@Test(dependsOnMethods="testMethod1",alwaysRun = true)
public void testMethod2() {
System.out.println("in testMethod2");
}
3. 重新运行未通过的测试
在大量的测试用例执行中,这种重新运行失败测试的能力显得尤为重要。这也是TestNG的优势。在JUnit 4中,如果测试套件包括1000项测试,其中3项失败了,修改错误以后,很可能不得不重新运行整个测试套件,会多耗费几个小时。
TestNG在执行中一旦出现失败,就会自动创建一个XML配置文件(一般命名为testng-failed.xml),对失败的测试加以说明。如果利用这个文件执行TestNG运行程序,就只运行失败的用例,而不是整个测试套件。同时,要求用例解耦。
4. 参数化测试
另一个有趣的TestNG特性是参数化测试。在JUnit中,如果想改变某个受测方法的参数组,就只能给每个不同的参数组编写一个测试用例。多数情况下,这不会带来太多麻烦。然而,我们有时会碰到一些情况,如其中的业务逻辑,要运行的测试数目变化范围很大。
TestNG可在XML配置文件中放入参数化数据,从而对不同的数据集重用同一个测试用例,这样可以对输入数据进行充分的测试,包括边界条件或无效数据的保护性验证测试。
完整的软件开发过程示例
假定须要实现字符串删除功能(将一段字符串中出现的所有某个单词全部删除掉),在已有类StringUtil.java中编程如图所示。
1) 为源代码编写测试程序
写好程序源代码之后,在对应的测试文件中,加上testDeleteString方法,其代码如图所示。这里只是在一个已有的测试类中添加一个测试方法。
采用3个case组合的目的是为了让源程序中的3种case都能被执行到,以提高测试代码的覆盖率。
2) 调试程序跟踪中间结果
2.1) 设置断点
只须双击设置断点的代码行的行号即可。例如在65行与69行前设置断点。如果在已设置好断点的行号前再次双击,断点就会取消。
2.2) 开始调试
右击须要进行调试的类名,这里是StringUtilTest.java。选择“Debug As”→“Open Debug Dialog…”,在弹出的窗口中检验是否为待调试的类,然后单击Debug。
3). 结果分析
查看中间结果。如果有错误,可以修改源程序或修改测试程序,直到完全正确。程序会首先停止在设置的断点上(本例为65行),然后按F6功能键,逐步执行,每步执行一行。可以在右上角Variable变量栏列表中看到每个变量运行时的值的变化情况,非常方便。从下图可以清楚地看到,程序运行到76行,其中间变量actual的值是“I am a teacher”,而期望的字符串(exceptedString)也是“I am a teacher”。
本文来自朱少民老师的《轻轻松松自动化测试》,如有侵权,请通知后删除。