9.2 JUnit4应用

  接下来通过对一个“计算器”类进行单元测试,来发现“计算器”类编写过程中出现的缺陷。

9.2.1 “计算器”类测试

  该“计算器”类功能简单,仅操作整数,并把运算结果存储在一个静态变量中。另外,这个“计算器”类有如下预设的错误。

  (1)减法并不返回一个有效的结果。

  (2)乘法还没有实现。

  (3)开方方法中存在一个无限循环错误。

  具体代码如下:

public class Calculator{

    //存储运算结果的静态变量

    private static int result;

    //加法

    public void add(int n){

        result = result + n;

    }

    //减法,有错误,应该是“result = result - n”

    public void subtract(int n){

        result = result - 1;

    }

    //乘法,此方法尚未实现

    public void multiply(int n){}

    //除法

    public void divide(int n){

        result = result / n;

    }

    //平方

    public void square(int n){

        result = n * n;

    }

    //开方,有死循环错误 

    public void squareRoot(int n){

        for(;;){}

    }

    //清除结果

    public void clear(){

        result = 0;

    }

    //获取运算结果

    public int getResult(){

        return result;

    }

}
copy

  使用JUnit4对“计算器”类进行单元测试,具体代码如下(本段代码中没有添加任何注释,希望大家在没有注释的情况下,尝试理解代码的含义):

import static org.junit.Assert.*;

import org.junit.*;

public class TestCalculator{

    Calculator calc = new Calculator();

    @Before

    public void setUp() throws Exception {

        System.out.println("测试前初始值置零!");

        calc.clear();

    }

    @After

    public void tearDown() throws Exception {

        System.out.println("测试后......");

    }

    @Test

    public void add(){

        calc.add(2);

        calc.add(3);

        int result = calc.getResult();

        assertEquals(5, result);

    }

    @Test

    public void subtract(){

        calc.add(10);

        calc.subtract(2);

        int result = calc.getResult();

        assertEquals(8, result);

    }

    @Test

    public void divide(){

        calc.add(8);

        calc.divide(2);

        assert calc.getResult() == 5;

    }

    @Test(expected = ArithmeticException.class)

    public void divideByZero(){

        calc.divide(0);

    }

    @Ignore("not Ready Yet Test Multiply")

    @Test

    public void multiply(){

        calc.add(10);

        calc.multiply(10);

        int result = calc.getResult();

        assertEquals(100, result);

    }

}
copy

  下面对这个单元测试类中用到的技术类进行解释。

  • 断言

  在 JUnit4 中,新集成了一个 assert 关键字(见案例中的 divide()方法),我们可以像使用assertEquals()方法一样来使用它,因为它们都抛出相同的异常java.lang.AssertionError。

  在JUnit4中,还引入了两个新的断言方法,它们专门用于数组对象的比较,其语法形式如下:

public static void assertEquals(String message,Object[] expected,Object[] actuals);

public static void assertEquals(Object[] expected,Object[] actuals);
copy

  原先JUnit3中的assertEquals(long,long)方法在JUnit4中都使用assertEquals (Object,Object)方法,对于assertEquals(byte,byte)、assertEquals(int,int)等也是如此,这是因为从JDK1.5开始支持自动拆箱、装箱机制。

  • 异常

  JUnit4的@Test注解支持可选参数,它可以声明一个测试方法应该抛出一个异常。如果这个方法不抛出或者如果它抛出一个与事先声明的不同的异常,那么该测试失败。在案例中(见案例中的divideByZero()方法),一个整数被零除应该抛出一个ArithmeticException异常,则该方法的@Test注解应该写成@Test(expected = ArithmeticException.class)。

  • 忽略测试

  在JUnit3中,临时禁止一个测试的方法是通过注释掉它或者改变命名约定,这样测试运行机就无法找到它。在JUnit4中,为了忽略一个测试,可以注释掉一个方法或者删除@Test注解(不能再改变命名约定,否则将抛出一个异常),该运行机将不理会也不报告这样一个测试。不过,在JUnit4中可以把@Ignore注解添加到@Test注解的前面或者后面,测试运行机将报告被忽略的测试的数目,以及运行的测试的数目和运行失败的测试数目。

  • 运行测试

  在JUnit3中,可以选择使用若干运行机,包括文本型、AWT或者Swing,在JUnit4中仅支持文本测试运行机。

  编译、运行程序,其运行结果如图9.5所示(截选部分内容)。从运行结果中可以看出测试失败的数目及详细信息。

图9.5 JUnit4测试“计算器”类
### 9.2.2 JUnit4知识拓展
  • 高级环境预设

  通过前面的学习可以知道,使用了@Before注解的方法在每个测试方法执行之前都要执行一次,使用了@After注解的方法在每个测试方法执行之后要执行一次。如果在测试时,仅需要分配和释放一次昂贵的资源,那么可以使用注解@BeforeClass 和@AfterClass,其含义为在所有的方法执行之前或之后执行一次。

  • 限时测试

  在Calculator类中,编写的开方方法代码如下:

public void squareRoot(int n){

for(;;){}

}
copy

  很显然,方法体内是一个死循环。如果使用JUnit对该方法执行单元测试,即需要在TestCalculator测试类中增加如下代码:


@Test

public void squareRoot(){

    calc.squareRoot(4);

    int result = calc.getResult();

    assertEquals(2, result);

}
copy

  再次编译、运行,其运行结果如图9.6所示。执行测试类,进入了死循环,不能正常退出。

图9.6 JUnit4测试死循环方法
  如何解决这个问题呢?尤其是对于那些逻辑很复杂,循环嵌套比较深的程序,很有可能出现死循环,因此一定要采取一些预防措施,JUnit4中的限时测试是一个很好的解决方案。如果给这些测试方法设定一个执行时间,并超过了这个时间,它们就会被系统强行终止,并且系统还会汇报该方法结束的原因是因为超时,这样就可以发现这些Bug了。要实现这一功能,只需要给@Test注解加一个参数即可,例如@Test(timeout = 1000),timeout参数表示设定的时间,单位为毫秒。编译、运行程序,运行结果如图9.7所示,JUnit4会再报告一个失败,失败的原因是超过了这个时间未获得预期结果。
图9.7 JUnit4限时测试
- 参数化测试

  在Calculator类中有一个求平方的方法square(),TestCalculator测试类还没有对它进行单元测试。假设现在为测试该方法设计3个测试用例,输入值分别是2、0、-3,预期结果分别是4、0、9,则需要在TestCalculator测试类中增加如下代码。

@Test

public void square1(){

    calc.square(2);

    int result = calc.getResult();

    assertEquals(4, result);

}

@Test

public void square2(){

    calc.square(0);

    int result = calc.getResult();

    assertEquals(0, result);

}

@Test

public void square3(){

    calc.square(-3);

    int result = calc.getResult();

    assertEquals(9, result);

}
copy

  前面在介绍自动化测试时提到过,如果步骤相同,只是输入数据和预期结果不一样的多次、重复的测试,可以考虑采用录制、回放的模式。录制一次执行步骤,然后将多组测试用例的输入数据和预期结果放入自动测试工具中,回放时每次执行一组输入数据,并将实际运行结果和预期结果进行比较判断,这样可以提高测试效率。

  基于同样的思路,JUnit4提出了参数化测试的概念,只写一个测试方法,把若干种情况作为参数传递进去,一次性完成测试。其具体代码如下(代码中的注释非常重要,请认真阅读):

import java.util.*;

import org.junit.*;

import org.junit.runner.RunWith;

import org.junit.runners.Parameterized;

import org.junit.runners.Parameterized.Parameters; 

import static org.junit.Assert.*;

//要为这个测试指定一个运行机,因为特殊的功能要用特殊运行机

@RunWith(Parameterized.class)

//为参数化测试专门生成一个新的类,不能与其他测试共用同一个类

public class TestSquare{

    Calculator calc = new Calculator();

    private int param;

    private int result;

    //定义测试数据集合,该方法可以任意命名,但是必须使用@Parameters注解进行修饰

    @Parameters public static Collection data(){

    return Arrays.asList(new Object[][]{ {2, 4},{0, 0},{-3, 9} });

}

//构造函数,其功能是对先前定义的两个参数进行初始化

public TestSquare(int param, int result) {

    this.param = param;

    this.result = result;

}

@Test

public void square(){

    calc.square(param);

    assertEquals(result, calc.getResult());

    }

}
copy

  编译、运行程序,运行结果如图9.8所示。

图9.8 JUnit4参数化测试
  关于JUnit4的测试运行机,这里做简要的补充说明。

  在 JUnit4 中,如果没有指定@RunWith,那么会使用一个默认运行机(org.junit.internal. runners.TestClassRunner)执行,但在参数化测试(使用@Parameterized注解)和马上要讲到的测试集测试(使用@Suite注解)的情况下,需要一个特定的运行机来执行测试用例。

  • 测试集

  之前编写了TestCalculator测试类,刚才又编写了TestSquare测试类,现在要执行这些测试的话,需要分别使用JUnit4命令执行对这两个测试类的单元测试。如果需要测试的测试类比较多,逐个执行会非常麻烦。

  在JUnit4之前的版本中,已经有测试集的概念,可以在一个测试集中运行若干个测试类,不过必须要在类中添加一个 suite()方法。而在 JUnit4 中,可以使用注解替代。为了运行TestCalculator和TestSquare这个两测试类,需要使用@RunWith和@Suite注解编写一个空类,具体代码如下:

import org.junit.runner.RunWith;

import org.junit.runners.Suite;

@RunWith(Suite.class)

@Suite.SuiteClasses({TestCalculator.class,TestSquare.class})

public class TestAllCalculator{}
copy