2.2.6 注释详解
Junit4 注解提供了书写单元测试的基本功能。在本节我们给大家介绍@BeforeClass, @AfterClass,@Before, @After 和@Test这几个基本注解。
@BeforeClass注解
被@BeforeClass注解的方法只被执行一次,是运行junit测试类时第一个被执行的方法。这样的方法被用作执行计算代价很大的任务,如打开数据库连接等。被@BeforeClass 注解的方法应该是静态的(即 static类型的).
@AfterClass注解
被@AfterClass注解的方法也只被执行一次,是运行junit测试类是最后一个被执行的方法。该类型的方法被用作执行类似关闭数据库连接的任务。被@AfterClass 注解的方法应该是静态的(即 static类型的).
@Before注解
被@Before 注解的方法是junit测试类中的任意一个测试方法执行前都会执行的方法,该类型的方法可以被用来为测试方法初始化所需的资源。
@After注解
被@After注解的方法是junit测试类中的任意一个测试方法执行后都会执行的方法, 即使被@Test 或 @Before修饰的测试方法抛出异常。该类型的方法被用来关闭由@Before注解修饰的测试方法打开的资源。
@Test 注解
被@Test注解的测试方法包含了真正的测试代码,并且会被Junit应用为要测试的方法。@Test注解有两个可选的参数:expected 表示此测试方法执行后应该抛出的异常,(值是异常名);timeout 检测测试方法的执行时间。
@Ignore注解
被@Ignore注解的测试方法包含了被忽略的测试代码,在测试过程中不会运行。
我们通过一段测试代码来进行检验,首先新建一个JDemo类如下所示:
package com.mooctest.util;
class JDemo extends Thread {
int result;
public int add(int a, int b) {
try {
sleep(1000);
result = a + b;
} catch (InterruptedException e) {
}
return result;
}
public int division(int a, int b) {
return result = a / b;
}
}
package com.mooctest.util;
import static org.junit.Assert.*;
import org.junit.*;
public class JDemoTest {
//测试BeforeClass注解
@BeforeClass
public static void setUpBeforeClass() throws Exception {
System.out.println("in BeforeClass================");
}
//测试AfterClass注解
@AfterClass
public static void tearDownAfterClass() throws Exception {
System.out.println("in AfterClass=================");
}
//测试Before注解
@Before
public void before() {
System.out.println("in Before");
}
//测试After注解
@After
public void after() {
System.out.println("in After");
}
//测试超时注解
@Test(timeout = 10000)
public void testadd() {
JDemo a = new JDemo();
assertEquals(6, a.add(3, 3));
System.out.println("in Test ----Add");
}
//测试Test注解
@Test
public void testdivision() {
JDemo a = new JDemo();
assertEquals(3, a.division(6, 2));
System.out.println("in Test ----Division");
}
//测试Ignore注解
@Ignore
@Test
public void test_ignore() {
JDemo a = new JDemo();
assertEquals(6, a.add(1, 5));
System.out.println("in test_ignore");
}
//测试异常注解
@Test(expected=ArithmeticException.class)
public void testException(){
assertEquals(2,new Calculate().divide(6, 0));
}
//测试Fail
@Test
public void teest_fail() {
fail();
}
}
通过eclipse运行代码,我们在控制台应该可以得到如下的输出:
in BeforeClass================
in Before
in After
in Before
in Test ----Add
in After
in Before
in After
in Before
in Test ----Division
in After
in AfterClass=================
Junit的测试结果如图2.20所示:
图2.20 注解程序运行结果图
可以看到测试的add和division的方法都没有问题,因为我们正确抓取异常的缘故,所以测试异常的程序也成功运行,被ignore注解的方法被系统忽略了,只有fail被报错了。
2.2.7 Failure和Error
在Junit模块中有两种报错模式,一种是Failure还有一种是Error。Failure一般由单元测试使用的断言方法判断失败所引起的,这就表示测试点发现了问题,就是说程序输出的结果和我们预期的不一样。而Error是由代码异常引起的,它可以产生与测试代码本身的错误,也可以是被测试代码中的一个隐藏的bug。对于软件测试工作者而言,测试用例不是用来证明你是对的,而是用来证明你没有错。
以简单的运算为例,我们先建立一个简单的Calculate类,如下:
package com.mooctest.util;
public class Calculate {
public int add(int a,int b){
return a+b;
}
public int divide(int a,int b){
return a/b;
}
}
针对这个类,我们新建一个专门测试Failure和Error的测试用例,如下:
package com.mooctest.util;
import static org.junit.Assert.*;
import org.junit.Test;
import com.mooctest.util.Calculate;
public class ErrorAndFailureTest {
//测试错误,报错Failure
@Test
public void testAdd(){
assertEquals(5,new Calculate().add(3, 3));
}
//测试失败,报错Error
@Test
public void testDivide(){
assertEquals(2,new Calculate().divide(6, 0));
}
}
2.2.8 参数化测试
Junit 4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同一个测试,因为针对同一个测试方法,不同的参数值会产生不同的结果,那么我们为了测试全面,会把多个参数值都写出来并进行断言测试。参数化测试就好比把一个“输入值,期望值”的集合传入给测试方法,达到一次性测试的目的。
你将遵循 5 个步骤来创建参数化测试:
1.用 @RunWith(Parameterized.class) 来注释 test 类。
2.创建一个由 @Parameters 注释的公共的静态方法,它返回一个对象的集合(数组)来作为测试数据集合。
3.创建一个公共的构造函数,它接受和一行测试数据相等同的东西。
4.为每一列测试数据创建一个实例变量。
5.用实例变量作为测试数据的来源来创建你的测试用例。
我们用一个简单的斐波那契数列的类作为例子,首先新建一个Fibonacci类,如下:
package com.mooctest.util;
class Fibonacci {
public static int compute(int input) {
int result;
switch (input) {
case 0:
result = 0;
break;
case 1:
case 2:
result = 1;
break;
case 3:
result = 2;
break;
case 4:
result = 3;
break;
case 5:
result = 5;
break;
case 6:
result = 8;
break;
default:
result = 0;
}
return result;
}
}
针对这个函数我们新建一个测试用例来进行参数化测试,新建测试用例FibonacciTest如下:
package com.mooctest.util;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class FibonacciTest {
@Parameters(name = "{index}: fib({0})={1}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 },
{ 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } });
}
private int input;
private int expected;
public FibonacciTest(int input, int expected) {
this.input = input;
this.expected = expected;
}
@Test
public void test() {
assertEquals(expected, Fibonacci.compute(input));
}
}
运行成功,Junit测试结果如图2.21所示:
图2.21 参数化测试结果图
2.2.9 打包测试
当我们面对较大的测试项目时,往往在项目中会有很多个测试用例,如果一个个测试显得很麻烦,因此出现了打包测试这个功能。顾名思义,打包测试就是一次性测试完成包中含有的所有测试用例。
以我们之前完成的3个测试用例CalculateTest、FibonacciTest以及JDemoTest为例,我们创建一个打包测试来对他们进行测试,代码如下:
package com.mooctest.util;
import static org.junit.Assert.*;
import org.junit.AfterClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses({CalculateTest.class,FibonacciTest.class, JDemoTest.class})
public class SuiteTest {
}
当打包测试运行完成后,打包运行结果与单独运行结果相同,具体运行结果如图2.22所示:
图2.22 打包测试运行结果图
2.2.10 异常处理
单元测试主要是被开发人员用来验证代码,很多时候单元测试可以检查抛出预期异常(expected exceptions)的代码。面对异常,我们需要给出正确的异常处理方法。在Java语言中,JUnit是一套标准的单元测试方案,它提供了很多验证抛出的异常的机制。
我们拿下面的代码作为例子,写一个测试,确保canVote() 方法返回true或者false, 同时你也能写一个测试用来验证这个方法抛出的IllegalArgumentException异常。
public class Student {
public boolean canVote(int age) {
if (i<=0) throw new IllegalArgumentException("age should be +ve");
if (i<18) return false;
else return true;
}
}
检查抛出的异常有三种方式,它们各自都有优缺点:
1.@Test(expected…)
@Test注解有一个可选的参数,”expected”允许你设置一个Throwable的子类。如果你想要验证上面的canVote()方法抛出预期的异常,我们可以这样写:
@Test(expected = IllegalArgumentException.class)
public void canVote_throws_IllegalArgumentException_for_zero_age() {
Student student = new Student();
student.canVote(0);
}
这种检查异常的方式直观明了,缺点是这个测试有一点误差,因为异常会在方法的某个位置被抛出,但不一定在特定的某行。
2.ExpectedException
在使用JUnit框架中的ExpectedException类前,需声明ExpectedException异常。
@Rule
public ExpectedException thrown= ExpectedException.none();
然后你可以使用更加简单的方式验证预期的异常。
@Test
public void canVote_throws_IllegalArgumentException_for_zero_age() {
Student student = new Student();
thrown.expect(IllegalArgumentException.class);
student.canVote(0);
}
或者可以设置预期异常的属性信息。
@Test
public void canVote_throws_IllegalArgumentException_for_zero_age() {
Student student = new Student();
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("age should be +ve");
student.canVote(0);
}
除了可以设置异常的属性信息之外,这种方法还有一个优点,它可以更加精确的找到异常抛出的位置。在上面的例子中,在构造函数中抛出的未预期的(unexpected) IllegalArgumentException 异常将会引起测试失败,我们希望它在canVote()方法中抛出。
3.Try/catch with assert/fail
在JUnit4之前的版本中,使用try/catch语句块检查异常
@Test
public void canVote_throws_IllegalArgumentException_for_zero_age() {
Student student = new Student();
try {
student.canVote(0);
} catch (IllegalArgumentException ex) {
assertThat(ex.getMessage(), containsString("age should be +ve"));
}
fail("expected IllegalArgumentException for non +ve age");
}
尽管这种方式很老了,不过还是非常有效的。主要的缺点就是很容易忘记在catch语句块之后需要写fail()方法,如果预期异常没有抛出就会导致信息的误报。我曾经就犯过这样的错误。
总之,这三种方法都可以测试预期抛出的异常,各有优缺点。大家可以根据自己的喜好进行选择使用。
2.2.11 限时测试
在我们进行代码开发的时候,经常会遇到死循环或者方法运行时间过长,为了防止出现死循环或者检查方法效率,我们就需要使用到限时测试,即超出设定时间即视为测试失败,共有两种写法。
第一种写法如下所示:
@Test(timeout=1000)
public void testWithTimeout() {
...
}
在Test注解之后加上一个测试的实现,比如说一千毫秒。
第二种写法则和异常处理的第二种方法类似,具体写法如下:
public class HasGlobalTimeout {
public static String log;
@Rule
public Timeout globalTimeout = new Timeout(10000); // 10 seconds max per method tested
@Test
public void testInfiniteLoop1() {
log += "ran1";
for (;;) {
}
}
@Test
public void testInfiniteLoop2() {
log += "ran2";
for (;;) {
}
}
}