title: 單元測(cè)試框架 JUnit 進(jìn)階指南
date: 2021/09/16 10:11
一、JUnit4
Runner
先問一個(gè)問題,你有沒有想過,為什么在單元測(cè)試中被 @Test注解的方法會(huì)被執(zhí)行?為什么@Before、@After、@BeforeClass、@AfterClass注解會(huì)被解析并在指定的時(shí)機(jī)執(zhí)行?為什么會(huì)記錄測(cè)試用例的耗時(shí)?
這是因?yàn)?JUnit 用例全部都是通過 Runner(運(yùn)行器)來執(zhí)行的,所謂運(yùn)行器的作用就是在單元測(cè)試執(zhí)行過程中提供一些特定的功能,JUnit 默認(rèn)使用 BlockJunit4ClassRunner 作為運(yùn)行器,也就是他為我們解析的以上注解,并且在指定時(shí)機(jī)執(zhí)行,通過監(jiān)聽器記錄開始和結(jié)束時(shí)間最終計(jì)算出用例的耗時(shí)等。
通過 @RunWith注解即可指定測(cè)試用例所使用的 Runner。
常見的執(zhí)行器:
| Runner | Description |
|---|---|
| BlockJunit4ClassRunner(基礎(chǔ)) | 解析 JUnit 提供的注解,封裝單元測(cè)試類的運(yùn)行過程 |
| Suite | 將一些 Runner 組合起來,一起執(zhí)行 |
| Parameterized | 繼承于 Suite,根據(jù)參數(shù)數(shù)組列表的個(gè)數(shù)創(chuàng)建多個(gè)基于該測(cè)試類的Runner |
| SpringRunner | 解析 Spring 提供的注解,進(jìn)行 DI |
BlockJunit4ClassRunner
該執(zhí)行器提供了對(duì) JUnit4 提供的注解解析的功能,包括@Before、@After、@BeforeClass、@AfterClass、@Rule等。
首先將斷點(diǎn)打到這里,往前看他的執(zhí)行流程:

大體流程如下:

圖來源:深入JUnit源碼之Runner
SpringRunner
Spring 提供了一個(gè) Runner SpringRunner,他繼承了 BlockJunit4ClassRunner,所以具有它的所有功能,并且擴(kuò)展了一些其他的功能,比如說:在單元測(cè)試方法執(zhí)行前,解析類上的@Autowired注解進(jìn)行 DI(當(dāng)然需要啟動(dòng) Spring 容器,容器中注入哪些 bean 則是通過 @SpringBootTest注解或@ContextConfiguration注解指定)。

TestContextManager
由于市面上有多種單元測(cè)試框架,Spring 將他們共有的功能抽取成了 TestContextManager,提供了如下方法:

而紅框中的那些功能又是委托給了 TestExecutionListener 來執(zhí)行的。
TestExecutionListener
public interface TestExecutionListener {
default void beforeTestClass(TestContext testContext) throws Exception {
}
default void prepareTestInstance(TestContext testContext) throws Exception {
}
default void beforeTestMethod(TestContext testContext) throws Exception {
}
default void beforeTestExecution(TestContext testContext) throws Exception {
}
default void afterTestExecution(TestContext testContext) throws Exception {
}
default void afterTestMethod(TestContext testContext) throws Exception {
}
default void afterTestClass(TestContext testContext) throws Exception {
}
}
Spring 提供了如下 TestExecutionListener,為解析 Spring 提供的各類可以在單元測(cè)試中使用的注解。

二、JUnit5
2.2.1 簡(jiǎn)介
JUnit 5 與以前版本的 JUnit 不同,拆分成由三個(gè)不同子項(xiàng)目的幾個(gè)不同模塊組成。
- JUnit Platform:用于JVM上啟動(dòng)測(cè)試框架的基礎(chǔ)服務(wù),提供命令行,IDE和構(gòu)建工具等方式執(zhí)行測(cè)試的支持。
- JUnit Jupiter:包含 JUnit 5 新的編程模型和擴(kuò)展模型,主要就是用于編寫測(cè)試代碼和擴(kuò)展代碼。
- JUnit Vintage:用于在JUnit 5 中兼容運(yùn)行 JUnit3.x 和 JUnit4.x 的測(cè)試用例。

2.2.2 新特性
提供全新的斷言和測(cè)試注解,支持測(cè)試類內(nèi)嵌
更豐富的測(cè)試方式:支持動(dòng)態(tài)測(cè)試,重復(fù)測(cè)試,參數(shù)化測(cè)試等
實(shí)現(xiàn)了模塊化,讓測(cè)試執(zhí)行和測(cè)試發(fā)現(xiàn)等不同模塊解耦,減少依賴
提供對(duì) Java 8 的支持,如 Lambda 表達(dá)式,Sream API等
2.2.3 常見用法介紹
接下來,我們看下 JUni 5 的一些常見用法,來幫助我們快速掌握 JUnit 5 的使用。
首先,在 Maven 工程里引入 JUnit 5 的依賴坐標(biāo),需注意的是當(dāng)前JDK 環(huán)境要在 Java 8 以上。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
第一個(gè)測(cè)試用例
引入JUnit 5,我們可以先快速編寫一個(gè)簡(jiǎn)單的測(cè)試用例,從這個(gè)測(cè)試用例來認(rèn)識(shí)初步下 JUnit 5:
@DisplayName("我的第一個(gè)測(cè)試用例")
public class MyFirstTestCaseTest {
@BeforeAll
public static void init() {
System.out.println("初始化數(shù)據(jù)");
}
@AfterAll
public static void cleanup() {
System.out.println("清理數(shù)據(jù)");
}
@BeforeEach
public void tearup() {
System.out.println("當(dāng)前測(cè)試方法開始");
}
@AfterEach
public void tearDown() {
System.out.println("當(dāng)前測(cè)試方法結(jié)束");
}
@DisplayName("我的第一個(gè)測(cè)試")
@Test
void testFirstTest() {
System.out.println("我的第一個(gè)測(cè)試開始測(cè)試");
}
@DisplayName("我的第二個(gè)測(cè)試")
@Test
void testSecondTest() {
System.out.println("我的第二個(gè)測(cè)試開始測(cè)試");
}
}
直接運(yùn)行這個(gè)測(cè)試用例,可以看到控制臺(tái)日志如下:

可以看到左邊一欄的結(jié)果里顯示測(cè)試項(xiàng)名稱就是我們?cè)跍y(cè)試類和方法上使用 @DisplayName 設(shè)置的名稱,這個(gè)注解就是 JUnit 5 引入,用來定義一個(gè)測(cè)試類并指定用例在測(cè)試報(bào)告中的展示名稱,這個(gè)注解可以使用在類上和方法上,在類上使用它就表示該類為測(cè)試類,在方法上使用則表示該方法為測(cè)試方法。
再來看下示例代碼中使用到的一對(duì)注解 @BeforeAll 和 @AfterAll ,它們定義了整個(gè)測(cè)試類在開始前以及結(jié)束時(shí)的操作,只能修飾靜態(tài)方法,主要用于在測(cè)試過程中所需要的全局?jǐn)?shù)據(jù)和外部資源的初始化和清理。與它們不同,@BeforeEach 和 @AfterEach 所標(biāo)注的方法會(huì)在每個(gè)測(cè)試用例方法開始前和結(jié)束時(shí)執(zhí)行,主要是負(fù)責(zé)該測(cè)試用例所需要的運(yùn)行環(huán)境的準(zhǔn)備和銷毀。
禁用執(zhí)行測(cè)試:@Disabled
當(dāng)我們希望在運(yùn)行測(cè)試類時(shí),跳過某個(gè)測(cè)試方法,正常運(yùn)行其他測(cè)試用例時(shí),我們就可以用上 @Disabled 注解,表明該測(cè)試方法處于不可用,執(zhí)行測(cè)試類的測(cè)試方法時(shí)不會(huì)被 JUnit 執(zhí)行。
下面看下使用 @Disbaled 之后的運(yùn)行效果,在原來測(cè)試類中添加如下代碼:
@DisplayName("我的第三個(gè)測(cè)試")
@Disabled
@Test
void testThirdTest() {
System.out.println("我的第三個(gè)測(cè)試開始測(cè)試");
}
運(yùn)行后看到控制臺(tái)日志如下,用 @Disabled 標(biāo)記的方法不會(huì)執(zhí)行,只有單獨(dú)的方法信息打?。?/p>

@Disabled 也可以使用在類上,用于標(biāo)記類下所有的測(cè)試方法不被執(zhí)行,一般使用對(duì)多個(gè)測(cè)試類組合測(cè)試的時(shí)候。
內(nèi)嵌測(cè)試類:@Nested
當(dāng)我們編寫的類和代碼逐漸增多,隨之而來的需要測(cè)試的對(duì)應(yīng)測(cè)試類也會(huì)越來越多。為了解決測(cè)試類數(shù)量爆炸的問題,JUnit 5提供了@Nested 注解,能夠以靜態(tài)內(nèi)部成員類的形式對(duì)測(cè)試用例類進(jìn)行邏輯分組。并且每個(gè)靜態(tài)內(nèi)部類都可以有自己的生命周期方法, 這些方法將按從外到內(nèi)層次順序執(zhí)行。此外,嵌套的類也可以用@DisplayName 標(biāo)記,這樣我們就可以使用正確的測(cè)試名稱。下面看下簡(jiǎn)單的用法:
@DisplayName("內(nèi)嵌測(cè)試類")
public class NestUnitTest {
@BeforeEach
void init() {
System.out.println("測(cè)試方法執(zhí)行前準(zhǔn)備");
}
@Nested
@DisplayName("第一個(gè)內(nèi)嵌測(cè)試類")
class FirstNestTest {
@Test
void test() {
System.out.println("第一個(gè)內(nèi)嵌測(cè)試類執(zhí)行測(cè)試");
}
}
@Nested
@DisplayName("第二個(gè)內(nèi)嵌測(cè)試類")
class SecondNestTest {
@Test
void test() {
System.out.println("第二個(gè)內(nèi)嵌測(cè)試類執(zhí)行測(cè)試");
}
}
}
運(yùn)行所有測(cè)試用例后,在控制臺(tái)能看到如下結(jié)果:

重復(fù)性測(cè)試:@RepeatedTest
在 JUnit 5 里新增了對(duì)測(cè)試方法設(shè)置運(yùn)行次數(shù)的支持,允許讓測(cè)試方法進(jìn)行重復(fù)運(yùn)行。當(dāng)要運(yùn)行一個(gè)測(cè)試方法 N次時(shí),可以使用 @RepeatedTest 標(biāo)記它,如下面的代碼所示:
@DisplayName("重復(fù)測(cè)試")
@RepeatedTest(value = 3)
public void i_am_a_repeated_test() {
System.out.println("執(zhí)行測(cè)試");
}
運(yùn)行后測(cè)試方法會(huì)執(zhí)行3次,在 IDEA 的運(yùn)行效果如下圖所示:

這是基本的用法,我們還可以對(duì)重復(fù)運(yùn)行的測(cè)試方法名稱進(jìn)行修改,利用 @RepeatedTest 提供的內(nèi)置變量,以占位符方式在其 name 屬性上使用,下面先看下使用方式和效果:
@DisplayName("自定義名稱重復(fù)測(cè)試")
@RepeatedTest(value = 3, name = "{displayName} 第 {currentRepetition} 次")
public void i_am_a_repeated_test_2() {
System.out.println("執(zhí)行測(cè)試");
}

@RepeatedTest 注解內(nèi)用 currentRepetition 變量表示已經(jīng)重復(fù)的次數(shù),totalRepetitions 變量表示總共要重復(fù)的次數(shù),displayName 變量表示測(cè)試方法顯示名稱,我們直接就可以使用這些內(nèi)置的變量來重新定義測(cè)試方法重復(fù)運(yùn)行時(shí)的名稱。
新的斷言
在斷言 API 設(shè)計(jì)上,JUnit 5 進(jìn)行顯著地改進(jìn),并且充分利用 Java 8 的新特性,特別是 Lambda 表達(dá)式,最終提供了新的斷言類: org.junit.jupiter.api.Assertions 。許多斷言方法接受 Lambda 表達(dá)式參數(shù),在斷言消息使用 Lambda 表達(dá)式的一個(gè)優(yōu)點(diǎn)就是它是延遲計(jì)算的,如果消息構(gòu)造開銷很大,這樣做一定程度上可以節(jié)省時(shí)間和資源。
現(xiàn)在還可以將一個(gè)方法內(nèi)的多個(gè)斷言進(jìn)行分組,使用 assertAll 方法如下示例代碼:
@Test
void testGroupAssertions() {
int[] numbers = {0, 1, 2, 3, 4};
Assertions.assertAll("numbers",
() -> Assertions.assertEquals(numbers[1], 1),
() -> Assertions.assertEquals(numbers[3], 3),
() -> Assertions.assertEquals(numbers[4], 4)
);
}
如果分組斷言中任一個(gè)斷言的失敗,都會(huì)將以 MultipleFailuresError 錯(cuò)誤進(jìn)行拋出提示。
超時(shí)操作的測(cè)試:assertTimeoutPreemptively
當(dāng)我們希望測(cè)試耗時(shí)方法的執(zhí)行時(shí)間,并不想讓測(cè)試方法無限地等待時(shí),就可以對(duì)測(cè)試方法進(jìn)行超時(shí)測(cè)試,JUnit 5 對(duì)此推出了斷言方法 assertTimeout,提供了對(duì)超時(shí)的廣泛支持。
假設(shè)我們希望測(cè)試代碼在一秒內(nèi)執(zhí)行完畢,可以寫如下測(cè)試用例:
@Test
@DisplayName("超時(shí)方法測(cè)試")
void test_should_complete_in_one_second() {
Assertions.assertTimeoutPreemptively(Duration.of(1, ChronoUnit.SECONDS), () -> Thread.sleep(2000));
}
這個(gè)測(cè)試運(yùn)行失敗,因?yàn)榇a執(zhí)行將休眠兩秒鐘,而我們期望測(cè)試用例在一秒鐘之內(nèi)成功。但是如果我們把休眠時(shí)間設(shè)置一秒鐘,測(cè)試仍然會(huì)出現(xiàn)偶爾失敗的情況,這是因?yàn)闇y(cè)試方法執(zhí)行過程中除了目標(biāo)代碼還有額外的代碼和指令執(zhí)行會(huì)耗時(shí),所以在超時(shí)限制上無法做到對(duì)時(shí)間參數(shù)的完全精確匹配。
異常測(cè)試:assertThrows
我們代碼中對(duì)于帶有異常的方法通常都是使用 try-catch 方式捕獲處理,針對(duì)測(cè)試這樣帶有異常拋出的代碼,而 JUnit 5 提供方法 Assertions#assertThrows(Class<T>, Executable) 來進(jìn)行測(cè)試,第一個(gè)參數(shù)為異常類型,第二個(gè)為函數(shù)式接口參數(shù),跟 Runnable 接口相似,不需要參數(shù),也沒有返回,并且支持 Lambda表達(dá)式方式使用,具體使用方式可參考下方代碼:
@Test
@DisplayName("測(cè)試捕獲的異常")
void assertThrowsException() {
String str = null;
Assertions.assertThrows(IllegalArgumentException.class, () -> {
Integer.valueOf(str);
});
}
當(dāng)Lambda表達(dá)式中代碼出現(xiàn)的異常會(huì)跟首個(gè)參數(shù)的異常類型進(jìn)行比較,如果不屬于同一類異常,就會(huì)控制臺(tái)輸出如下類似的提示:org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <IllegalArgumentException> but was: <...Exception>
2.2.4 擴(kuò)展機(jī)制 @ExtendWith
擴(kuò)展機(jī)制與 Runner 的功能類似,在單元測(cè)試執(zhí)行的過程中實(shí)現(xiàn)一些功能。

在 SpringBoot2.0 中的@SpringBootTest注解就標(biāo)注了 @ExtendWith({SpringExtension.class}),使單元測(cè)試伴隨著 Spring 環(huán)境(不需要 @RunWith 注解)。
為什么 SpringBoot2.0 中的
@SpringBootTest注解中標(biāo)注了 @ExtendWith,但是 SpringBoot1.5 中沒有標(biāo)注 @RunWith 注解,還需要自己手動(dòng)添加 @RunWith 注解?我覺得可能是1.5 的時(shí)候想著要兼容所有單元測(cè)試庫,而在 2 的時(shí)候選用 junit 作為默認(rèn),還不如直接就標(biāo)注上去。