JUnit是一款常用于Java工程的开源单元测试框架(Open Source Unit Test Framework)。软件测试大致可在三个层次上进行。系统测试(System Test)用于测试整个系统的功能;集成测试(Integration Test)用于测试两个或者多个子模块集成之后的功能;单元测试(Unit Test)用于测试系统中每个最小单元的功能。因此,从上述的测试目的和定义来看,单元测试的对象是项目中最小的、不可再分的单元。在Java项目中,单元测试的对象是一个个独立的类。单元测试一般由开发人员维护和执行。测试人员一般不执行单元测试。
JUnit 5由三个模块组成。JUnit Platform模块提供了测试基础功能和服务。JUnit Jupiter模块提供了测试框架模型与测试扩展模型。在本章中的例子都是使用JUnit Jupiter完成的。JUnit Vintage提供了向后兼容(Backward Compatibility);开发人员可在JUnit 5中运行JUnit 3和JUnit 4的测试用例(Test Cases)。
如果在Maven项目中使用JUnit 5的话,需要在pom.xml中添加如下依赖。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
如果在Gradle项目中使用JUnit 5的话,需要在build.gradle中添加如下依赖。
repositories {
mavenCentral()
}
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.7.0')
}
为了展示JUnit 5的用法,我们将使用JUnit 5来测试下面的功能代码。这段代码实现了一个斐波那契数列生成器。当创建一个FibonacciGenerator对象之后,通过调用get()方法能够生成相应的斐波那契数。
package com.littlewaterdrop;
public class FibonacciGenerator {
public int get(int i) throws IllegalArgumentException {
if (i < 0) {
throw new IllegalArgumentException();
} else if (i == 0) {
return 0;
} else if (i == 1) {
return 1;
} else {
return this.get(i-2) + this.get(i-1);
}
}
public static void main(String[] args) {
FibonacciGenerator fibonacciGenerator = new FibonacciGenerator();
for (int i = 0; i < 10; i++) {
System.out.println(String.format("Fibonacci(%d) = %d", i, fibonacciGenerator.get(i)));
}
}
}
上述的代码输出如下结果。
Fibonacci(0) = 0
Fibonacci(1) = 1
Fibonacci(2) = 1
Fibonacci(3) = 2
Fibonacci(4) = 3
Fibonacci(5) = 5
Fibonacci(6) = 8
Fibonacci(7) = 13
Fibonacci(8) = 21
Fibonacci(9) = 34
无论是Maven项目还是Gradle项目,项目中的测试代码结构是相同的。例如,在如下的项目中,功能代码放在src/main/java目录下。源代码按照其包(package)的结构存放在相应的目录下。因此,FibonacciGenerator.java存放在src/main/java/com/littlewaterdrop目录下。
单元测试的代码存放在src/test/java目录下。通常的做法是按照功能代码的结构来组织单元测试代码,并且单元测试类与被测试类应一一对应,并按照其包的结构安排。例如,FibonacciGenerator.java放在com.littlewaterdrop包中。那么,测试FibonacciGenerator的单元测试类可命名为FibonacciGeneratorTest,存放在com.littlewaterdrop目录下。
以此类推,如果我们有一个新类com.littlewaterdrop.util.StringUtil,那么,它的单元测试类可命名为StringUtilTest,放在包com.littlewaterdrop.util中。
注意,上述仅仅是一种常见的、惯用的方法。开发人员也可以按照自己的喜好和习惯安排组织单元测试代码。
A-Java-Project
|- src
| |- main
| | |- java
| | | |- com
| | | | |- littlewaterdrop
| | | | | |- FibonacciGenerator.java
| |- test
| | |- java
| | | |- com
| | | | |- littlewaterdrop
| | | | | |- FibonacciGeneratorTest.java
在执行单元测试的过程中,因为每个单元测试类都是相互独立的。因此,单元测试的结果应不依赖于其执行顺序。所以,在默认情况下,JUnit会按照某一内部顺序依次执行每一个测试类。类似的,在一个测试类中,每个测试用例也应是相互独立的,JUnit也会按照某一内部顺序依次执行测试类中的每一个测试用例。
在执行一个测试类时,JUnit会创建新对象来执行每一个测试用例。换句话说,在调用测试函数之前,JUnit会创建出一个新的测试类对象。然后,在这个新的对象上调用测试函数。这样做的目的是隔离(Isolation)测试用例的副作用(Side Effect),避免一个测试用例的行为影响另一个测试用例的测试结果。
开发人员可以为每一个测试类设置BeforeAll方法、BeforeEach方法、AfterAll方法和AfterEach方法。BeforeAll和AfterAll方法是测试类的静态方法。JUnit会在执行这个测试类之前和完毕之后分别调用这两个方法。BeforeEach和AfterEach方法是测试类的成员方法。JUnit会在执行每一个测试用例之前和完毕之后分别执行这两个方法。因此,BeforeAll和AfterAll最多只会执行一次;而BeforeEach和AfterEach可能会支持多次。
下面是测试FibonacciGenerator的单元测试类FibonacciGeneratorTest。它使用了标注(Annotation)来标记BeforeAll、AfterAll、BeforeEach、AfterEach方法。在BeforeEach方法中,每次创建一个新的FibonacciGenerator对象。
FibonacciGeneratorTest类还包含了两个测试用例:testFibonacciOfZero()和testFibonacciOfOne()方法。它们分别测试Fibonacci(0)和Fibonacci(1)的值。这两个方法使用了@Test标注来将自己标识为测试用例。因此,JUnit在运行此测试类时,会运行这两个测试用例。
在测试用例中,开发人员需要使用assert*()方法来检查测试的结果。如果所有的assert()方法全部通过的话,则该测试用例会被认为测试通过。如果当有任何一个assert()方法失败时,该测试用例会被认为失败。
assert*()方法是类org.junit.jupiter.api.Assertions的静态成员方法。所以,开发人员可以使用import static语句将它们导入进来。在使用时,可以直接使用方法名称调用assert*()方法。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@DisplayName("Test Fibonacci(0)")
void testFibonacciOfZero() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@DisplayName("Test Fibonacci(1)")
void testFibonacciOfOne() {
assertEquals(1, fibonacciGenerator.get(1));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
@AfterAll
static void tearDownAll() {
}
}
在上述的测试用例中,我们测试了Fibonacci(0)和Fibonacci(1)的值是否为0和1。标注@DisplayName可用于在测试报告中输出个性化的测试用例信息。
JUnit提供了许多方便的assert*()方法。例如,assertEquals()用于比较两个对象是否相等。更多的assert*()方法如下表所示。
方法 | 描述 |
---|---|
assertEquals() | 断言给定的两个对象相等。 |
assertTrue() | 断言给定的条件为true。 |
assertFalse() | 断言给定的条件为false。 |
assertNull() | 断言给定的对象引用为null。 |
assertNotNull() | 断言给定的对象引用不为null。 |
assertSame() | 断言给定的两个对象引用指向同一个对象。 |
assertNotSame() | 断言给定的两个对象引用指向不同的对象。 |
assertArrayEquals() | 断言给定的两个数组相等。 |
assertAll() | 断言给定的所有条件全部为true。 |
assertTimeout() | 断言给定的任务执行时间不超过指定的时间。 |
assertThrow() | 断言给定的任务会抛出指定的异常。 |
fail() | 直接判定测试用例失败。 |
所以,我们可以在测试代码中使用上述的assert*()方法来检验测试结果。
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@DisplayName("Test Fibonacci(0)")
void testFibonacciOfZero() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@DisplayName("Test Fibonacci(0)、Fibonacci(1)、Fibonacci(2)")
void testFibonacciOfZeroOneTwo() {
assertAll(
() -> assertEquals(0, fibonacciGenerator.get(0)),
() -> assertEquals(1, fibonacciGenerator.get(1)),
() -> assertEquals(1, fibonacciGenerator.get(2))
);
}
@Test
@DisplayName("Test Speed of Fibonacci(0)")
void testSpeedOfFibonacci() {
// 当任务没有返回值时
assertTimeout(
java.time.Duration.ofSeconds(1),
() -> fibonacciGenerator.get(0)
);
// 当任务有返回值时
int result = assertTimeout(
java.time.Duration.ofSeconds(1),
() -> { return fibonacciGenerator.get(1);}
);
assertEquals(1, result);
}
@Test
@DisplayName("Test invalid parameter")
void testFibonacciOfInvalidParameter() {
assertThrows(IllegalArgumentException.class, () -> fibonacciGenerator.get(-1));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
@AfterAll
static void tearDownAll() {
}
}
JUnit还提供了预设条件系列函数assume*()。有时,一些测试用例只有在一定的运行环境和条件下运行才有意义。那么,开发人员可以使用assume*()函数来检查运行环境和条件。
开发人员往往容易混淆assume*()函数和assert*()函数,因为它们有着十分相似的接口,并且都可用于测试用例函数中。一旦测试失败,JUnit会终止执行当前的测试用例。但是,它们有着本质的区别。assume*()用于检查预设条件,当条件不满足时,JUnit会跳过当前测试用例。具体的说,当assume*()函数断言失败后,JUnit会跳出当前的测试用例;当前的测试用例不会认为是失败的。然而,当assert*()函数断言失败后,该测试用例会被认为是失败的。这是assume*()与assert*()函数最重要的区别。
我们使用如下的例子来解释如何使用assume*()。我们假设只需要测试0-10以内整数的斐波那契的值。所以,在testFibonacci()测试用例中,我们先使用assumeTrue()测试变量i的值。如果i的值不在[0, 10]之内,那么就不执行该测试用例。
assume*()方法是类org.junit.jupiter.api.Assumptions的静态成员方法。所以,开发人员可以使用import static语句将它们导入进来。在使用时,可以直接使用方法名称调用assume*()方法。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private int i;
private FibonacciGenerator fibonacciGenerator = null;
@BeforeEach
void init() {
i = 0;
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@DisplayName("Test Fibonacci(0) to Fibonacci(10)")
void testFibonacci() {
assumeTrue(this.i >= 0 && this.i <=10);
assertEquals(0, fibonacciGenerator.get(this.i));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
}
JUnit还支持根据外部条件来选择运行测试用例。
开发人员可以使用@EnabledOnOs和@DisabledOnOs标注来根据操作系统类型来选择运行测试用例。例如,testOnlyOnLinux()测试用例仅在Linux环境下运行。testNotOnWindows()测试用例不会在Windows上运行。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@EnabledOnOs(OS.LINUX) // 仅在Linux环境下运行
void testOnlyOnLinux() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@EnabledOnOs({OS.LINUX, OS.MAC}) // 仅在Linux或者Mac环境下运行
void testOnlyOnLinuxOrMac() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@DisabledOnOs(OS.WINDOWS) // 不在Windows环境下运行
void testNotOnWindows() {
assertEquals(0, fibonacciGenerator.get(0));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
}
开发人员可以使用@EnabledOnJre和@DisabledOnJre标注来根据Java运行环境来选择运行测试用例。例如,testOnlyOnJava8()测试用例仅在Java 8环境下运行。testNotOnJava9()测试用例不在Java 9环境下运行。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.condition.EnabledOnJre;
import org.junit.jupiter.api.condition.DisabledOnJre;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@EnabledOnJre(JRE.JAVA_8) // 只在JAVA 8环境下执行
void testOnlyOnJava8() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@EnabledForJreRange(min = JRE.JAVA_8, max = JRE.JAVA_14) // 只在JAVA 8至JAVA 14环境下执行
void testOnlyOnJava8To11() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@EnabledForJreRange(min = JRE.JAVA_8) // 只在JAVA 8或之上的环境下执行
void testOnlyOnJava8OrAbove() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@DisabledOnJre(JRE.JAVA_9) // 不在Java 9的环境下执行
void testNotOnJava9() {
assertEquals(0, fibonacciGenerator.get(0));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
}
开发人员可以使用@EnabledIfSystemProperty和@DisabledIfSystemProperty标注来根据系统属性的值来选择运行测试用例。在Java应用程序中,系统属性可以通过System.getProperty()和System.setProperty()来设置和获取。如下面的代码所示,onlyOn64BitArchitectures()测试用例仅在系统变量os.arch包含字符串"64"时才运行。当os.arch包含字符串"32"时,不会运行notOn32BitArchitectures()测试用例。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@DisabledIfSystemProperty(named = "os.arch", matches = ".*32.*")
void notOn32BitArchitectures() {
assertEquals(0, fibonacciGenerator.get(0));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
}
开发人员可以使用@EnabledIfEnvironmentVariable和@DisabledIfEnvironmentVariable标注来根据环境属性的值来选择运行测试用例。在Java应用程序中,环境属性可以通过System.getEnv()来获取。如下面的代码所示,onlyOnDevelopmentServer()测试用例仅在环境变量ENV为字符串"developemnt"时才运行。当环境变量ENV包含字符串"Production"时不执行notOnProductionServer()测试用例。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "development")
void onlyOnDevelopmentServer() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*Production.*")
void notOnProductionServer() {
assertEquals(0, fibonacciGenerator.get(0));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
}
开发人员还可以使用@EnabledIf和@DisabledIf标注来自定义逻辑选择运行测试用例。如下面的代码所示,开发人员可以将自定义的逻辑写入一个成员方法中,该方法的返回值为boolean类型。JUnit会根据该成员函数的返回值来判断是否运行测试用例。例如,在下面的代码中,JUnit会根据成员函数customCondition()的返回值来选择是否运行enabled()和disabled()测试用例。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.condition.DisabledIf;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@EnabledIf("customCondition") // 当函数customCondition()返回true时运行
void enabled() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@DisabledIf("customCondition") // 当函数customCondition()返回true时不运行
void disabled() {
assertEquals(0, fibonacciGenerator.get(0));
}
public boolean customCondition() {
return true;
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
}
开发人员可以使用@Order标注来明确指定某一测试类中的测试用例的执行顺序。例如,在如下的代码中,firstTestCase()会在secondTestCase()之前运行。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.TestMethodOrder;
import com.littlewaterdrop.FibonacciGenerator;
@TestMethodOrder(OrderAnnotation.class)
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@Order(10)
void firstTestCase() {
assertEquals(0, fibonacciGenerator.get(0));
}
@Test
@Order(20)
void secondTestCase() {
assertEquals(1, fibonacciGenerator.get(1));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
}
@RepeatedTest标注允许开发人员反复运行某一测试用例。在下面的示例中,repeatThisTestcase10Times()测试用例会运行10次。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@RepeatedTest(10)
void repeatThisTestcase10Times() {
assertEquals(0, fibonacciGenerator.get(0));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
}
@Timeout标注允许开发人员为测试用例设置一个超时时间。如果运行超时,该测试用例会被记录为失败。@Timeout与assertTimeout()方法不同之处在于,@Timeout设置的是整个测试用例的超时时间,而assertTimeout()方法设置的是一个具体执行过程或者步骤的超时时间。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.DisplayName;
import java.util.concurrent.TimeUnit;
import com.littlewaterdrop.FibonacciGenerator;
@DisplayName("Test FibonacciGenerator")
public class FibonacciGeneratorTest {
private FibonacciGenerator fibonacciGenerator = null;
@BeforeEach
void init() {
fibonacciGenerator = new FibonacciGenerator();
}
@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds100Milliseconds() {
assertEquals(0, fibonacciGenerator.get(0));
}
@AfterEach
void tearDown() {
fibonacciGenerator = null;
}
}
本章介绍了JUnit框架的使用方法。JUnit是一款流行的Java应用程序的单元测试框架,它主要以类为单位测试Java代码的功能。为了更好的完成单元测试,开发人员往往会在单元测试中使用Mock技术,以覆盖更多的测试场景。因此,我们将在下一章中介绍与JUnit结合使用的Mock框架。
注册用户登陆后可留言