JUnit单元测试教程——终极指南
说明
本教程翻译自Java Code Geeks,原文网址:https://www.javacodegeeks.com/2014/11/junit-tutorial-unit-testing.html#suite_tests。翻译的过程中,有少部分内容是译者添加的解释说明和对原文章中错误地方的修正。水平有限,如果有错误的地方,希望能在我的博客里告诉我,谢谢。
相关的例子不想自己写的同学们可以去这里下载:http://download.csdn.net/detail/rainnnbow/9603847
1. 单元测试简介
1.1 什么是单元测试?
一个单元可以是一个方法、一个类、一个包、 或者一个子系统。所以术语单元测试表示对代码中的很小的单元的测试行为,以确保它们能够按预期工作。例如,我们给定一些输入,测试是否是预期的输出,或者测试一个条件是真还是假。
这种测试行为能帮助开发者发现隐藏在代码后面的错误,并提高代码质量。单元测试也被用来确保代码能够按照预期工作,以防止未来可能的变化。
1.2 测试覆盖
总的来说,开发者社区对代码测试要达到多少比例存在不同的观点。许多开发者认为代码的测试覆盖率要达到100%。另一些则认为50%或者更低就可以了。但无论如何,你都应该为你的代码中复杂的以及重要的部分编写测试。
1.3 Java中的单元测试
Java中最著名的测试框架是JUnit,本指南是专门介绍JUnit的,接下来的部分将详细介绍该测试框架的更多细节。Java中另一个著名的测试框架是TestNG。
2. JUnit简介
JUnit是一个开源的测试框架,它被用来编写和运行可重复的自动化测试,保证我们的代码按照预期的执行。JUnit被广泛使用:它可以在Java程序中单独使用(使用命令行)或者结合IDE使用,如Eclipse。
JUnit提供以下功能:
- 断言测试的预期结果。
- 共享测试数据的特性。
- 使用Test suites简化测试用例的组织和运行。
- 图形化和文本化测试用例?(Graphical and textual test runners.)
JUnit可以用来测试以下内容:
- 一个对象
- 部分对象——一个方法或几个相互影响的方法
- 多个对象之间的交互
2.1 使用Eclipse实现简单JUnit测试例子
在这一节我们将展示一个简单的JUnit例子。首先来看一下我们要测试的类:
Calculate.java:
package com.javacodegeeks.junit;
public class Calculate {
public int sum(int var1, int var2) {
System.out.println("Adding values: " + var1 + " + " + var2);
return var1 + var2;
}
}
在上面的源码中,我们看到该类有一个public的方法sum(),方法接受两个int参数,相加之后返回结果。我们将测试这个方法。因此我们需要创建另一个类,并在这个类中为测试Calculate类中的所有需要测试的方法一一编写测试方法(在这个例子中,我们只有一个方法需要测试)。这是最普通的使用方法。当然如果一个方法非常复杂,我们可以为这个方法编写多个测试方法。编写测试用例的更多细节,我们将在下一节介绍。下面的CalculateTest.java类就是我们的测试类:
package com.javacodegeeks.junit;
import static org.junit.Assert.*;
import org.junit.Test;
public class CalculateTest {
Calculate calculation = new Calculate();
int sum = calculation.sum(2, 5);
int testSum = 7;
@Test
public void testSum() {
System.out.println("@Test sum(): " + sum + " = " + testSum);
assertEquals(sum, testSum);
}
}
让我们来解释一下上面的代码。首先我们看到在testSum()方法上有一个@Test注解。这个注解表示这个方法可以作为一个测试用例运行。所以testSum()方法被用来测试sum()方法。我们还看到了assertEquals(sum, testSum)方法,assertEquals ([String message], object expected, object actual)方法用来接手两个对象,并断言这两个对象是否相等。
运行该测试类,Run As–>JUnit Test,程序输出将如下所示:
为了查看JUnit测试的实际结果,Eclipse提供了JUnit窗口来显示测试结果。在这个用例中,测试成功,JUnit窗口不会显示任何错误或失败信息,所以我们看到了如下所示的界面:
如果我们改变一下代码:
int testSum = 10;
修改代码后两个整数的测试不再相等,程序输出将如下所示:
同时,在JUnit窗口中会显示测试出错,并给出错误信息:
2.2 JUnit注解
本节我们将介绍JUnit4支持的基本注解,下表是对这些索引的总结:
让我们来看一个包含了以上注解的测试类的例子:
AnnotationsTest.java:
package com.javacodegeeks.junit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
public class AnnotationsTest {
private ArrayList testList;
/**
* if the method isn't static, throw following exception:
* java.lang.Exception: Method onceExecutedBeforeAll() should be static
*/
@BeforeClass
public static void onceExecutedBeforeAll() {
System.out.println("@BeforeClass: onceExecutedBeforeAll");
}
@Before
public void executedBeforeEach() {
testList = new ArrayList();
System.out.println("@Before: executedBeforeEach");
}
/**
* If the method isn't static, throw following exception:
* java.lang.Exception: Method onceExecutedAfterAll() should be static
*/
@AfterClass
public static void onceExecutedAfterAll() {
System.out.println("@AfterClass: onceExecutedAfterAll");
}
@After
public void executedAfterEach() {
testList.clear();
System.out.println("@After: executedAfterEach");
}
@Test
public void EmptyCollection() {
assertTrue(testList.isEmpty());
System.out.println("@Test: EmptyArrayList");
}
@Test
public void OneItemCollection() {
testList.add("oneItem");
assertEquals(1, testList.size());
System.out.println("@Test: OneItemArrayList");
}
@Ignore
public void executionIgnored() {
System.out.println("@Ignore: This execution is ignored");
}
}
执行以上测试,控制台输出如下所示:
JUnit窗口显示如下信息:
2.3 Junit断言
本节我们将展示一些断言方法。所有这些方法由Assert类提供,Assert类继承自java.lang.Object。这些断言能帮助你编写测试用例,监测到失败的测试。下表是对一些最常用的断言方法的详细介绍:
让我们看一个上述断言的例子:
AssertionsTest.java
package com.javacodegeeks.junit;
import static org.junit.Assert.*;
import org.junit.Test;
public class AssertionsTest {
@Test
public void test() {
String obj1 = "junit";
String obj2 = "junit";
String obj3 = "test";
String obj4 = "test";
String obj5 = null;
int var1 = 1;
int var2 = 2;
int[] arithmetic1 = {1,2,3};
int[] arithmetic2 = {1,2,3};
assertEquals(obj1, obj2);
assertSame(obj3, obj4);
assertNotSame(obj2, obj4);
assertNotNull(obj1);
assertNull(obj5);
assertTrue(var1 != var2);
assertFalse(var1 == var2);
assertArrayEquals(arithmetic1, arithmetic2);
}
}
从以上类中我们可以看到断言方法是怎样工作的。
- assertEquals():如果两个对象相等,方法正常返回;否则在JUnit窗口中将显示错误,该测试将被中断。
- assertSame()与assertNotSame():测试两个对象的引用是否指向了同一个对象。
- assertNull()与assertNotNull():测试一个变量是否是null或非null。
- assertTrue()与assertFalse():测试一个条件或变量时true或false.。
- assertArrayEquals():比较两个数组是否相等。如果相等,方法正常返回;否则在JUnit窗口中将显示错误,该测试将被中断。
3. 使用Eclipse实现完整的JUnit例子
本节我们将展示一个使用JUnit的完整例子。我们将讲解怎样创建和运行测试用例,以及怎样使用JUnit的注解和断言。
3.1 创建工程
创建一个名为JUnitGuide的java工程。创建一个包com.javacodegeeks.junit用来放被测试的类。对于测试类,一个好的实践是创建一个专用于测试的源码包。这样被测试类和测试类就被分在了不同的源码包中。创建test包:右键单击你的工程, New–>Source Folder,命名源码包为test。
提示:创建源码包的另一种方式:右键单击你的工程PropertiesJava Build Path,选择Source 项,然后Add FolderCreate New Folder。输入源码包名,点击finish。
你能看到在工程里有两个源码包:
你可以在test源码包中也创建一个名称为com.javacodegeeks.junit的包,这样你的测试类就不会被放在默认的包中。
3.2 创建要被测试的Java类
右击src源码包,创建一个名为FirstDayAtScholl.java的类。该类的public方法将被测试。
FirstDayAtSchool.java
3.3 创建并运行JUnit测试用例
为已经存在的类FirstDayAtSchool.java创建测试用例:选中FirstDayAtSchool.java类,右击选择NewJUnit Test Case。改变源码包,将测试用例类放在test源码包中。确保选择“JUnit 4 test”选项。
点击“Finish”。如果你的项目classpath下还没有包含JUnit4,将会弹出如下所示的信息让你将JUnit4 Library添加进你的工程中。
(译者注:也可以直接在test源码包中像创建普通类一样创建一个测试用例类。只要该类中的方法用@Test注解标注上,就能引入JUnit框架,并成为测试用例。)
以下就是我们创建的名为FirstDayAtSchoolTest.java的测试用例。
FirstDayAtSchoolTest.java
右键Run AsJUnit Test,运行测试用例。
程序输出如下所示:
JUnit窗口显示如下:
此时JUnit窗口没有任何错误或失败。如果我们改变其中一个数组,让它包含比预期更多的元素:
再次运行测试类,JUnit窗口将显示有一个失败的测试用例:
或者,我们再次改变数组,让它包含与预期不同的元素:
再次运行测试类,JUnit窗口将再次显示一个测试用例fail:
3.4 使用@Ignore注解
在上面的例子中,让我们看看怎样使用@Ignore注解。在测试类FirstDayAtSchoolTest.java的testAddPencils()方法上标注@Ignore注解。通过这种方式,我们将该测试方法忽略掉,测试时不会执行。
执行测试,输出如下所示:@Ignore注解的测试方法没有执行。
JUnit窗口显示如下:
现在,我们去掉testAddPencils()方法上的@Ignore注解,将@Ignore注解标注在FirstDayAtSchoolTest.java类上。
运行该测试类发现:整个测试用例将不会执行,所以控制台上没有任何输出。JUnit窗口显示如下信息:
3.5 创建测试套件(suite tests)
本节,我们将展示如何创建测试套件。测试套件是来自于不同类的多个测试用例的集合。使用@Runwith和@Suite注解可以让这些测试类一起运行。当你有许多测试类并且你希望这些测试类一起执行时,这是非常有用的。
在上一节类的基础上,我们可以创建两个测试类。一个测试prepareMyBag()方法,另一个测试addPencils()方法。两个测试类如下所示:
PrepareMyBagTest.java.
AddPencilsTest.java.
我们来创建一个测试套件类来运行以上两个测试类。测试套件类SuiteTest.java如下所示:
SuiteTest.java.
使用@Suite.SuiteClasses注解来定义哪些测试类将被包含进测试套件一起执行。
运行该测试套件,这两个测试类将按照在@Suite.SuiteClasses注解中声明的顺序执行。
执行完毕后JUnit窗口显示信息如下:
3.6 创建参数化测试(parameterized tests)
本节我们将展示如何创建参数化的测试。我们将使用前面的2.1节中的类作为被测试类。
什么时候一个测试类才能被认为是一个参数化的测试类呢?当测试类满足下列条件的时候:
该类使用@RunWith(Parameterized.class)标注;
@RunWith注解告诉JUnit使用该注解标明的执行器执行测试用例,而不是使用JUnit内置的默认执行器。Parameterized是JUnit中的一个执行器,该执行器将使用一组不同的输入来多次执行一个相同的测试用例。
该类有唯一的构造函数来创建测试数据;
该类有一个static方法用来产生并返回测试数据,该方法使用@Parameters注解标注;
该类有一个使用@Test标注的测试用例(译者注:废话吗这不是)。
我们创建了一个新的测试类CalculateTest.java。该类遵循以上的规则。该类源码如下:
上如代码满足以上的所有规则。addedNumbers方法使用@Parameters标注,返回数组的集合。每一个数组包含一次测试执行的I/O数据。数组中元素的个数必须和构造函数中参数的个数一致。在该例子中,一个数组包含三个元素,两个代表要被相加的数,一个代表结果。
运行该测试用例,控制台输出如下所示:
JUnit窗口输出信息如下:
从上述信息可以看出,该测试用例执行了4次,这正是使用@Parameters注解标注的方法中输入的测试数据的个数。
3.7 规则(Rules)
本节我们将介绍JUnit4的一个新特性,叫做规则(Rules)。Rules允许我们灵活的增加或重新定义每一个测试方法的行为。为达到这个目的,使用@Rule注解标注测试类中public的域(fields)。这些域必须是MethodRule类型的,他们标明了一个测试方法的运行或结果报告作出了哪些改变。一个测试方法可以应用多个MethodRule。MethodRule接口有许多实现,如ErrorCollector,该规则允许测试用例在发现第一次错误是可以继续运行;ExpectedException,该规则允许我们明确预期异常的类型及异常信息;TestName,该规则允许我们在测试方法中获取到当前测试方法的名称;等等其他规则。除了JUnit已经定义的规则,开发者也可以自定义规则并在测试用例中使用。
下面,我们展示一个使用已经存在的TestName规则的例子。当一个测试类开始执行时TestName被调用。
NameRuleTest.java
如上所示:@Rule注解标注了一个public的域name,该域是MethodRule类型的。确切的说,该类是MethodRule接口的实现类MethodRule类型。然后我们就可以使用name在我们的测试方法中获取当前正在运行的测试方法的名字了。
(译者注:关于Rules的详细说明,请查看JUnit4官方文档:https://github.com/junit-team/junit4/wiki/Rules。运行里面的例子,有疑问就修改例子测试,一目了然。Rules还是有不少用处的。)
3.8 策略(Categories)
JUnit的另一个新特性是策略,这允许你将特定的测试放在一起形成一个分组(group),并可以包含或排除某些分组。例如你可以将运行慢的测试用例与运行快的测试用例分开。JUnit提供了@Category注解为一个测试用例标明策略。以下是使用该新特性的例子。该特性从JUnit4.8开始支持。
首先,定义两个策略,FastTests和SlowTests。一个策略可以是一个类或者一个借口。
以上的代码中,我们在A类的b()方法上使用了@Category注解,标注该方法在SlowTests策略中。所以,@Category注解不仅可以标注整个类,也可以标注某一个测试方法。
以上的代码中,我们看到整个B类使用了@Category标注。在类上标注后,该类的所有测试方法都会自动包含进@Category中声明的策略中。我们也可以看到,一个类或者一个测试方法可以属于多个策略。
以上代码中,我们看到有一个名为SlowTestSuite_1的测试套件。一般情况下,我们认为策略就是一种测试套件。在该套件中,我们使用@IncludeCategory注解标明哪些策略将会被执行。在该例中,属于SlowTests策略的所有方法将会被执行。因此,A类中的b()方法和B类中的c方法将被执行,他们都属于SlowTests策略。
以上代码中,我们对测试套件做了一些修改,我们添加了一个新的注解@ExcludeCategory,该注解表明那些策略将被排除。在本例中,只有A类的b()方法会执行,因为他是唯一只属于SlowTests策略,并不属于FastTests策略的测试方法。
通过以上两个例子我们看到,A类的a()方法从未执行,因为它不属于任何策略。
5. 命令行运行JUnit测试
通过使用org.junit.runner.JUnitCore 类,可以在Eclipse之外运行测试用例。该类提供了runClasses()方法允许执行一个或多个测试类。runClasses()方法的返回类型是一个org.junit.runner.Result对象。该对象可以搜集所有测试的信息。如果有失败的测试用例,你可以使用org.junit.runner.notification.Failure对象来获取失败的测试用例的描述。
下面的流程展示了如何在Eclipse之外运行你的测试。
创建一个新类JunitRunner.java如下所示:
JunitRunner.java
我们选择运行名为AssertionsTest的测试类。
打开DOS窗口,并定位到JunitRunner.java所在的目录(译者注:本人的目录是**\JUnitGuide\test\com\javacodegeeks\junit)。
编译测试类和Runner类。
javac -classpath “D:\learnTech\test\junit-4.11.jar”;”D:\learnTech\test\hamcrest-core-1.3.0.jar”; AssertionsTest.java JunitRunner.java
和在Eclipse中一样,我们需要将JUnit的jar包包含进classpath中。运行完上述命令。在Junitrunner.java所在的目录下就生成了AssertionsTest.class 和JunitRunner.class两个class文件。如下所示:
在DOS窗口中,退回到test源码目录(译者注:本人的目录是**\JUnitGuide\test)。
运行JunitRunner类。
java -classpath “D:\learnTech\test\junit-4.11.jar”;”D:\learnTech\test\hamcrest-core-1.3.0.jar”; JunitRunner
输出结果为: