使用 JUnit 进行 Java 代码的单元测试

下载安装 JUnit 的相关文件

首先咱们须要先下载相应的 JUnit 相关的 JAR 包,下载的过程能够去 JUnit 的官方网站,也能够直接经过 Maven 资源仓库来完成,我这里直接经过开源中国社区在国内的Maven 镜像下载了 JUnit-4.8.2.jar 的版本,以下图所示: html

直接搜索关键字"junit"便可,咱们能够从搜索结果中找到红色方框1中的对应项,选中以后就会在左下方列出目前能够得到的 junit  的全部版本,这里我选择了 4.8.2 的版本(红色方框2),而后点击右侧的 Download 按钮便可下载这個 JAR 包,不过依我在实践中的经验,最好同时下载对应版本的 javadoc 和 source 两個包,前者是文档,后者是对应的源代码,而后将它们直接引入到咱们的 eclipse 的工程中便可。我这里建立了壹個普通的 Java Project ,并给它取名 junit-study ,导入 JAR 包以后的样子以下图所示。到此,咱们使用 JUnit 的准备工做已经作完。 java

使用简单的 @Test 注解实现咱们的测试方法的编写和执行

准备工做作好以后,接下来咱们就能够开始尝试编写壹個简单的测试代码了。首先,咱们编写了壹個 Calculator 类,并提供五個方法分别完成加减乘除以及求平方的运算。代码以下: 数据库

package net.oschina.bairrfhoinn.main;

public class Calculator {

	public void add(int n){
		result += n;
	}
	
	public void substract(int n){
		result -= n;
	}
	
	public void multiply(int n){
		result *= n;
	}
	
	public void divide(int n){
		result /= n;
	}
	
	public void square(int n){
		result = n * n;
	}
	
	public int getReuslt(){
		return result;
	}
	
	public void clear(){
		result = 0;
	}
	
	private static int result;
}
在测试类中用到了JUnit4框架,天然要把相应地Package包含进来。最主要地一个Package就是org.junit.*。把它包含进来以后, 绝大部分功能就有了。还有一句话也很是地重要“import static org.junit.Assert.*;”,咱们在测试的时候使用的壹系列assertEquals()方法就来自这个包。你们注意壹下,这是壹個静态包 含(static),是JDK5中新增添的壹個功能。也就是说,assertEquals是Assert类中的壹系列的静态方法,壹般的使用方式是 Assert. assertEquals(),可是使用了静态包含后,前面的类名就能够省略了,使用起来更加的方便。
另外要注意的是,咱们的测试类是壹個独立的类,没有任何父类。测试类的名字也能够任意命名,没有任何局限性。因此咱们不能经过类的声明来判断它是不 是一个测试类,它与普通类的区别在于它内部的方法的声明,咱们接着会讲到。在测试类中,并非每壹個方法都是用于测试的,因此咱们必须使用“注解”来明确 代表哪些是测试方法。“注解”也是JDK5的壹個新特性,用在此处很是恰当。咱们能够看到,在某些方法的前有@Before、@Test、 @Ignore 等字样,这些就是注解,以壹個“@”做为开头。这些注解都是JUnit4自定义的,熟练掌握这些注解的含义,对于编写恰当的测试类很是重要。
接下来咱们建立壹個测试类 CalculatorTest.java,代码以下:
package net.oschina.bairrfhoinn.test;

import static org.junit.Assert.*;

import org.junit.Test;

import net.oschina.bairrfhoinn.main.Calculator;

public class CalculatorTest {
	
	private static Calculator calculator = new Calculator();
	
	@Test
	public void testAdd(){
		calculator.add(7);
		calculator.add(8);
		assertEquals(15, calculator.getReuslt());
	}
}
首先,咱们要在方法的前面使用@Test标注,以代表这是壹個测试方法。对于方法的声明也有以下要求:名字能够随便取,没有任何限制,可是返回值必须为void,并且不能有任何参数。若是违反这些规定,会在运行时抛出壹個异常。至于方法内该写些什么,那就要看你须要测试些什么了。好比上述代码中,咱们想测试壹下add()方法的功能是否正确,就在测试方法中调用几回add函数,初始值为0,先加7,再加8,咱们期待的结果应该是15。若是最终实际结果也是15,则说明add()方法是正确的,反之说明它是错的。assertEquals(15, calculator.getResult());就是用来判断期待结果和实际结果是否相等,其中第壹個参数填写期待结果,第二個参数填写实际结果,也就是经过计算获得的结果。这样写好以后,JUnit 会自动进行测试并把测试结果反馈给用户。

若是想运行它,能够在 eclipse 的资源管理器中选择该类文件,而后点击右键,选择 Run As->JUnit Test 便可看到运行结果以下图所示: 数组

使用@Test 的 Ignore 属性指定测试时跳过某個方法

若是你在写程序前作了很好的规划,那么哪些方法是什么功能都应该实现而且肯定下来。所以,即便该方法还没有完成,他的具体功能也是肯定的,这也就意味着你能够为他编写测试用例。可是,若是你已经把该方法的测试用例写完,但该方法还没有完成,那么测试的时候无疑是“失败”。这种失败和真正的失败是有区别的,所以 JUnit 提供了壹种方法来区别他们,那就是在这种测试函数的前面加上 @Ignore 标注,这个标注的含义就是“某些方法还没有完成,暂不参与这次测试”。这样的话测试结果就会提示你有几個测试被忽略,而不是失败。壹旦你完成了相应函数,只须要把@Ignore标注删去,就能够进行正常的测试。
好比说上面的测试类 Calculator.java 中,假设咱们的 Calculator 类的 multiply() 方法没有实现,咱们能够在测试类 CalculatorTest 中先写以下测试代码: 框架

package net.oschina.bairrfhoinn.test;
import static org.junit.Assert.*;
import org.junit.Ignore;
import org.junit.Test;

import net.oschina.bairrfhoinn.main.Calculator;

public class CalculatorTest {
	
	private static Calculator calculator = new Calculator();
	...	//此处代码省略
	@Ignore("method square() not implemented, please test this later...")
	@Test
	public void testSquare(){
		calculator.square(3);
		assertEquals(9, calculator.getReuslt());
	}
}

咱们再运行壹次测试,会看到以下结果,从图中能够很明显的看出,方法testSquare() 上的 @Ignore 注解已经生效了,运行时直接跳过了它,而方法testAdd()仍然正常的运行并经过了测试。 eclipse

使用注解 @Before 和 @After 来完成前置工做和后置工做

前置工做一般是指咱们的测试方法在运行以前须要作的壹些准备工做,如数据库的链接、文件的加载、输入数据的准备等须要在运行测试方法以前作的事情,都属于 前置工做;相似的,后置工做则是指测试方法在运行以后的壹些要作的事情,如释放数据库链接、输入输出流的关闭等;好比咱们上面的测试,因为只声明了壹個 Calculator 对象,他的初始值是0,可是测试完加法操做后,他的值就不是0了;接下来测试减法操做,就必然要考虑上次加法操做的结果。这绝对是壹個很糟糕的设计!咱们 很是但愿每壹個测试方法都是独立的,相互之间没有任何耦合度。所以,咱们就颇有必要在执行每壹個测试方法以前,对Calculator对象进行壹個“复 原”操做,以消除其余测试形成的影响。所以,“在任何壹個测试方法执行以前必须执行的代码”就是壹個前置工做,咱们用注解 @Before 来标注它,以下例子所示: maven

package net.oschina.bairrfhoinn.test;
...
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

public class CalculatorTest {
    ...//这里省略部分代码
    @Before
    public void setUp() throws Exception {
        calculator.clear();
    }
    
    @After
    public void tearDown() throws Exception {
        System.out.println("will do sth here...");
    }
    ...//这里省略部分代码
}


另外要说的是,注解 @Before 是定义在 org.junit.Before 这個类中的,所以使用时须要将其引入咱们的代码中。这样作了以后,每次咱们运行测试方法时,JUnit 都会先运行 setUp() 方法将 result 的值清零。不过要注意的是,这里再也不须要 @Test 注解,由于这并非壹個 test,只是壹個前置工做。同理,若是“在任何测试执行以后须要进行的收尾工做,咱们应该使用 @After 来标注,方法与它相似。因为本例比较简单,不须要用到此功能,因此咱们只是简单了给它添加了壹個 tearDown() 方法并在收尾时打印壹句话到控制台,而且使用 @After 来注解这個方法。 ide

使用@BeforeClass 和 @AfterClass 来完成只须要执行壹次的前置工做和后置工做

上面咱们提到了两個注解 @Before 和 @After ,咱们来看看他们是否适合完成以下功能:有壹個类负责对大文件(超过500 MB)进行读写,他的每壹個方法都是对文件进行操做。换句话说,在调用每壹個方法以前,咱们都要打开壹個大文件并读入文件内容,这绝对是壹個很是耗费时的 操做。若是咱们使用 @Before 和 @After ,那么每次测试都要读取壹次文件,效率及其低下。因此咱们但愿的是,在全部测试壹开始读壹次文件,全部测试结束以后释放文件,而不是每次测试都读文件。 JUnit的做者显然也考虑到了这个问题,它给出了@BeforeClass 和 @AfterClass 两個注解来帮咱们实现这个功能。从名字上就能够看出,用这两個注解标注的函数,只在测试用例初始化时执行 @BeforeClass 方法,当全部测试执行完毕以后,执行 @AfterClass 进行收尾工做。在这里要注意壹下,每個测试类只能有壹個方法被标注为 @BeforeClass 或 @AfterClass,并且该方法必须是 public static 类型的。 函数

使用@Test 的属性 timeout 来完成限时测试,以检测代码中的死循环

如今假设咱们的 Calculator 类中的 square() 方法是個死循环,那应该怎么办呢,好比说像下面这样: 测试

public void square(int n){
	for(;;){}
}


若是测试的时候遇到死循环,你的脸上绝对不会露出笑容的。所以,对于那些逻辑很复杂,循环嵌套比较深的、有可能出现死循环的程序,所以壹定要采起壹些预防 措施。限时测试是壹個很好的解决方案。咱们给这些测试函数设定壹個预期的执行时间,超过了这壹时间,他们就会被系统强行终止,而且系统还会向你汇报该函数 结束的缘由是由于超时,这样你就能够发现这些 Bug 了。要实现这壹功能,只须要给 @Test 标注加壹個参数timeout便可,代码以下:

@Test(timeout=2000L)
public void testSquare() {
	calculator.square(3);
	assertEquals(9, calculator.getReuslt());
}
timeout参数代表了你预计该方法运行的时长,单位为毫秒,所以2000就表明2秒。如今咱们让这個测试方法运行壹下,看看失败时是什么效果:

使用@Test 的属性expected来监控测试方法中可能会抛出的某些异常

JAVA中的异常处理也是壹個重点,所以你常常会编写壹些须要抛出异常的函数。若是你以为壹個函数应该抛出异常,可是它没抛出,这算不算 Bug 呢?这固然是Bug,JUnit 也考虑到了这壹点,而且能够帮助咱们找到这种 Bug。例如,咱们写的计算器类有除法功能,若是除数是壹個0,那么必然要抛出“除0异常”。所以,咱们颇有必要对这些进行测试。代码以下:

@Test(expected=java.lang.ArithmeticException.class)
public void testDivide(){
	calculator.divide(0);
}
如上述代码所示,咱们须要使用@Test注解中的expected属性,将咱们要检验的异常(这里是 java.lang.ArithmeticException)传递给他,这样 JUnit 框架就能自动帮咱们检测是否抛出了咱们指定的异常。


指定 JUnit 运行测试用例时的 Runner

你们有没有想过这個问题,当你把测试代码提交给JUnit框架后,框架是如何来运行你的代码的呢?答案就是Runner。在JUnit中有不少个 Runner,他们负责调用你的测试代码,每壹個Runner都有其各自的特殊功能,你要根据须要选择不一样的Runner来运行你的测试代码。可能你会觉 得奇怪,前面咱们写了那么多测试,并无明确指定壹個Runner啊?这是由于JUnit中有壹個默认的Runner,若是你没有指定,那么系统会自动使 用默认Runner来运行你的代码。换句话说,下面两段代码含义是彻底壹样的:

import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class CalculatorTest {
	...//省略此处代码
}

//用了系统默认的JUnit4.class,运行效果彻底壹样
public class CalculatorTest {
	...//省略此处代码
}
从上述例子能够看出:
一、要想指定壹個 Runner ,须要使用 @RunWith 标注,而且把你所指定的 Runner 类名做为参数传递给它,在JUnit4.8.2的版本中,系统提供了若干能够直接使用的Runner类型,它们的定义都在包org.junit.runners下面。
二、注解 @RunWith 是用来修饰类的,而不是用来修饰函数的。只要对壹個类指定了 Runner ,那么这个类中的全部函数都被这个 Runner 来调用。
三、在使用注解@RunWith时,要在头部包含相应的包名,上面的例子对这壹点写的很清楚了。
接下来,我会向大家展现其余 Runner 的特有功能。

使用参数化测试完成须要录入大量数据的测试

你可能遇到过这样的函数,它的参数有许多特殊值,或者说他的参数分为不少个区域。好比,壹個对考试分数进行评价的函数,返回值分别为“优秀,良好,壹般, 及格,不及格”,所以你在编写测试的时候,至少要写5個测试,把这五种状况都包含了,这确实是壹件很麻烦的事情。这里咱们仍然使用先前的例子,测试壹下 square()这个函数,暂且分三类:正数、0、负数。测试代码以下:

package net.oschina.bairrfhoinn.test;

import static org.junit.Assert.*;
import net.oschina.bairrfhoinn.main.Calculator;

import org.junit.Before;
import org.junit.Test;

public class AdvancedTest {
	private static Calculator calculator = new Calculator();

	@Before
	public void setUp() throws Exception {
		calculator.clear();
	}
	
	@Test
	public void testSquare1(){
		calculator.square(2);
		assertEquals(4, calculator.getReuslt());
	}
	
	@Test
	public void testSquare2(){
		calculator.square(0);
		assertEquals(0, calculator.getReuslt());
	}
	
	@Test
	public void testSquare3(){
		calculator.square(-3);
		assertEquals(9, calculator.getReuslt());
	}
}
为了简化相似的测试,JUnit4提出了“参数化测试”的概念,只写壹個测试函数,把这若干种状况的输入参数和预期的运行结果放在集合中,而后将这個集合做为参数传递进去,壹次性的完成测试。代码以下:
package net.oschina.bairrfhoinn.test;

import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.Collection;
import net.oschina.bairrfhoinn.main.Calculator;
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 SquareTest {
	
	@Parameters
	public static Collection prepareData(){
		return Arrays.asList(new Object[][]{{2,4},{0, 0},{-3, 9}});
	}
	
	public SquareTest(int param, int result){
		this.param = param;
		this.result = result;
	}
	
	@Test
	public void square(){
		calculator.square(param);
		assertEquals(result, calculator.getReuslt());
	}
	
	private int param;
	private int result;
	private static Calculator calculator = new Calculator();
}
下面咱们对上述代码进行分析。首先,你要为这种测试专门生成壹個新的类,而不能与其余测试共用同壹個类,此例中咱们定义了壹個 SquareTest 类。而后,你要为这個类指定壹個 Runner,而不能使用默认的 Runner 了,由于特殊的功能要用特殊的Runner 了。@RunWith(Parameterized.class) 这条语句就是为这个类指定了壹個 Parameterized 的 Runner。第二步,定义壹個待测试的类,而且定义两个变量,壹個用于存放参数,另外壹個用于存放期待的结果。接下来,定义测试数据的集合,也就是上述 的prepareData() 方法,该方法实际能够任意命名,可是必须使用 @Parameters 注解进行修饰。这個方法的框架就不予解释了,你们只须要注意其中的数据,是壹個二维数组,数据两两壹组,每组中的这两个数据,壹個是参数,另外壹個是你预 期的结果。好比咱们的第壹组{2, 4},2 就是参数,4 就是预期的结果。这两个数据的顺序无所谓,谁前谁后均可以。以后是构造函数,其功能就是对先前定义的两个参数进行初始化。在这里你可要注意一下参数的顺序 了,要和上面的数据集合的顺序保持壹致。若是前面的顺序是{参数,期待的结果},那么你构造函数的顺序也要是“构造函数(参数, 期待的结果)”,反之亦然。最后就是写壹個简单的测试用例了,和前面介绍过的写法彻底壹样,在此就很少说。

将全部的测试用例打包后集中测试

经过前面的介绍咱们能够感受到,在壹個项目中,只写壹個测试类是不可能的,咱们会写出不少不少个测试类。但是这些测试类必须壹個壹個的执行,也是比较麻烦 的事情。鉴于此,JUnit 为咱们提供了打包测试的功能,将全部须要运行的测试类集中起来,壹次性的运行完毕,大大的方便了咱们的测试工做。具体代码以下:

package net.oschina.bairrfhoinn.test;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorTest.class, SquareTest.class, AdvancedTest.class})
public class AllCalculatorTests {
	//这里什么都不作
}

咱们能够看到,这个功能也须要使用壹個特殊的Runner,所以咱们须要向@RunWith 注解传递壹個参数Suite.class。同时,咱们还须要另外壹個注解@Suite.SuiteClasses,来代表这個类是壹個打包测试类。咱们把 须要打包的类做为参数传递给该标注就能够了。有了这两个注解以后,就已经完整的表达了全部的含义,所以下面的类已经可有可无,随便起壹個类名,内容所有为 空既可。想对全部测试类进行测试时,只须要运行这壹個测试类便可。运行以后的结果以下图所示。