title: 單元測試框架 JUnit 進(jìn)階指南
date: 2021/09/16 10:11
一、JUnit4
Runner
先問一個問題,你有沒有想過,為什么在單元測試中被 @Test
注解的方法會被執(zhí)行奶栖?為什么@Before
、@After
门坷、@BeforeClass
宣鄙、@AfterClass
注解會被解析并在指定的時機(jī)執(zhí)行?為什么會記錄測試用例的耗時拜鹤?
這是因為 JUnit 用例全部都是通過 Runner(運行器)來執(zhí)行的框冀,所謂運行器的作用就是在單元測試執(zhí)行過程中提供一些特定的功能流椒,JUnit 默認(rèn)使用 BlockJunit4ClassRunner
作為運行器敏簿,也就是他為我們解析的以上注解,并且在指定時機(jī)執(zhí)行宣虾,通過監(jiān)聽器記錄開始和結(jié)束時間最終計算出用例的耗時等惯裕。
通過 @RunWith
注解即可指定測試用例所使用的 Runner。
常見的執(zhí)行器:
Runner | Description |
---|---|
BlockJunit4ClassRunner(基礎(chǔ)) | 解析 JUnit 提供的注解绣硝,封裝單元測試類的運行過程 |
Suite | 將一些 Runner 組合起來蜻势,一起執(zhí)行 |
Parameterized | 繼承于 Suite,根據(jù)參數(shù)數(shù)組列表的個數(shù)創(chuàng)建多個基于該測試類的Runner |
SpringRunner | 解析 Spring 提供的注解鹉胖,進(jìn)行 DI |
BlockJunit4ClassRunner
該執(zhí)行器提供了對 JUnit4 提供的注解解析的功能握玛,包括@Before
够傍、@After
、@BeforeClass
挠铲、@AfterClass
冕屯、@Rule
等。
首先將斷點打到這里拂苹,往前看他的執(zhí)行流程:
大體流程如下:
圖來源:深入JUnit源碼之Runner
SpringRunner
Spring 提供了一個 Runner SpringRunner
安聘,他繼承了 BlockJunit4ClassRunner
,所以具有它的所有功能瓢棒,并且擴(kuò)展了一些其他的功能浴韭,比如說:在單元測試方法執(zhí)行前,解析類上的@Autowired
注解進(jìn)行 DI(當(dāng)然需要啟動 Spring 容器脯宿,容器中注入哪些 bean 則是通過 @SpringBootTest
注解或@ContextConfiguration
注解指定)念颈。
TestContextManager
由于市面上有多種單元測試框架,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 提供的各類可以在單元測試中使用的注解窘面。
二翠语、JUnit5
2.2.1 簡介
JUnit 5 與以前版本的 JUnit 不同,拆分成由三個不同子項目的幾個不同模塊組成财边。
- JUnit Platform:用于JVM上啟動測試框架的基礎(chǔ)服務(wù)肌括,提供命令行,IDE和構(gòu)建工具等方式執(zhí)行測試的支持酣难。
- JUnit Jupiter:包含 JUnit 5 新的編程模型和擴(kuò)展模型谍夭,主要就是用于編寫測試代碼和擴(kuò)展代碼。
- JUnit Vintage:用于在JUnit 5 中兼容運行 JUnit3.x 和 JUnit4.x 的測試用例憨募。
2.2.2 新特性
提供全新的斷言和測試注解紧索,支持測試類內(nèi)嵌
更豐富的測試方式:支持動態(tài)測試,重復(fù)測試菜谣,參數(shù)化測試等
實現(xiàn)了模塊化珠漂,讓測試執(zhí)行和測試發(fā)現(xiàn)等不同模塊解耦,減少依賴
提供對 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>
第一個測試用例
引入JUnit 5寞缝,我們可以先快速編寫一個簡單的測試用例,從這個測試用例來認(rèn)識初步下 JUnit 5:
@DisplayName("我的第一個測試用例")
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)前測試方法開始");
}
@AfterEach
public void tearDown() {
System.out.println("當(dāng)前測試方法結(jié)束");
}
@DisplayName("我的第一個測試")
@Test
void testFirstTest() {
System.out.println("我的第一個測試開始測試");
}
@DisplayName("我的第二個測試")
@Test
void testSecondTest() {
System.out.println("我的第二個測試開始測試");
}
}
直接運行這個測試用例仰泻,可以看到控制臺日志如下:
可以看到左邊一欄的結(jié)果里顯示測試項名稱就是我們在測試類和方法上使用 @DisplayName 設(shè)置的名稱第租,這個注解就是 JUnit 5 引入,用來定義一個測試類并指定用例在測試報告中的展示名稱我纪,這個注解可以使用在類上和方法上慎宾,在類上使用它就表示該類為測試類,在方法上使用則表示該方法為測試方法浅悉。
再來看下示例代碼中使用到的一對注解 @BeforeAll 和 @AfterAll 趟据,它們定義了整個測試類在開始前以及結(jié)束時的操作,只能修飾靜態(tài)方法术健,主要用于在測試過程中所需要的全局?jǐn)?shù)據(jù)和外部資源的初始化和清理汹碱。與它們不同,@BeforeEach 和 @AfterEach 所標(biāo)注的方法會在每個測試用例方法開始前和結(jié)束時執(zhí)行荞估,主要是負(fù)責(zé)該測試用例所需要的運行環(huán)境的準(zhǔn)備和銷毀咳促。
禁用執(zhí)行測試:@Disabled
當(dāng)我們希望在運行測試類時,跳過某個測試方法勘伺,正常運行其他測試用例時跪腹,我們就可以用上 @Disabled 注解,表明該測試方法處于不可用飞醉,執(zhí)行測試類的測試方法時不會被 JUnit 執(zhí)行冲茸。
下面看下使用 @Disbaled 之后的運行效果,在原來測試類中添加如下代碼:
@DisplayName("我的第三個測試")
@Disabled
@Test
void testThirdTest() {
System.out.println("我的第三個測試開始測試");
}
運行后看到控制臺日志如下缅帘,用 @Disabled 標(biāo)記的方法不會執(zhí)行轴术,只有單獨的方法信息打印:
@Disabled 也可以使用在類上钦无,用于標(biāo)記類下所有的測試方法不被執(zhí)行逗栽,一般使用對多個測試類組合測試的時候。
內(nèi)嵌測試類:@Nested
當(dāng)我們編寫的類和代碼逐漸增多失暂,隨之而來的需要測試的對應(yīng)測試類也會越來越多彼宠。為了解決測試類數(shù)量爆炸的問題,JUnit 5提供了@Nested 注解趣席,能夠以靜態(tài)內(nèi)部成員類的形式對測試用例類進(jìn)行邏輯分組兵志。并且每個靜態(tài)內(nèi)部類都可以有自己的生命周期方法醇蝴, 這些方法將按從外到內(nèi)層次順序執(zhí)行宣肚。此外,嵌套的類也可以用@DisplayName 標(biāo)記悠栓,這樣我們就可以使用正確的測試名稱霉涨。下面看下簡單的用法:
@DisplayName("內(nèi)嵌測試類")
public class NestUnitTest {
@BeforeEach
void init() {
System.out.println("測試方法執(zhí)行前準(zhǔn)備");
}
@Nested
@DisplayName("第一個內(nèi)嵌測試類")
class FirstNestTest {
@Test
void test() {
System.out.println("第一個內(nèi)嵌測試類執(zhí)行測試");
}
}
@Nested
@DisplayName("第二個內(nèi)嵌測試類")
class SecondNestTest {
@Test
void test() {
System.out.println("第二個內(nèi)嵌測試類執(zhí)行測試");
}
}
}
運行所有測試用例后按价,在控制臺能看到如下結(jié)果:
重復(fù)性測試:@RepeatedTest
在 JUnit 5 里新增了對測試方法設(shè)置運行次數(shù)的支持,允許讓測試方法進(jìn)行重復(fù)運行笙瑟。當(dāng)要運行一個測試方法 N次時楼镐,可以使用 @RepeatedTest 標(biāo)記它,如下面的代碼所示:
@DisplayName("重復(fù)測試")
@RepeatedTest(value = 3)
public void i_am_a_repeated_test() {
System.out.println("執(zhí)行測試");
}
運行后測試方法會執(zhí)行3次往枷,在 IDEA 的運行效果如下圖所示:
這是基本的用法框产,我們還可以對重復(fù)運行的測試方法名稱進(jìn)行修改,利用 @RepeatedTest 提供的內(nèi)置變量错洁,以占位符方式在其 name
屬性上使用秉宿,下面先看下使用方式和效果:
@DisplayName("自定義名稱重復(fù)測試")
@RepeatedTest(value = 3, name = "{displayName} 第 {currentRepetition} 次")
public void i_am_a_repeated_test_2() {
System.out.println("執(zhí)行測試");
}
@RepeatedTest 注解內(nèi)用 currentRepetition
變量表示已經(jīng)重復(fù)的次數(shù),totalRepetitions
變量表示總共要重復(fù)的次數(shù)屯碴,displayName
變量表示測試方法顯示名稱描睦,我們直接就可以使用這些內(nèi)置的變量來重新定義測試方法重復(fù)運行時的名稱。
新的斷言
在斷言 API 設(shè)計上导而,JUnit 5 進(jìn)行顯著地改進(jìn)忱叭,并且充分利用 Java 8 的新特性,特別是 Lambda 表達(dá)式今艺,最終提供了新的斷言類: org.junit.jupiter.api.Assertions 韵丑。許多斷言方法接受 Lambda 表達(dá)式參數(shù),在斷言消息使用 Lambda 表達(dá)式的一個優(yōu)點就是它是延遲計算的虚缎,如果消息構(gòu)造開銷很大埂息,這樣做一定程度上可以節(jié)省時間和資源。
現(xiàn)在還可以將一個方法內(nèi)的多個斷言進(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)
);
}
如果分組斷言中任一個斷言的失敗千康,都會將以 MultipleFailuresError 錯誤進(jìn)行拋出提示。
超時操作的測試:assertTimeoutPreemptively
當(dāng)我們希望測試耗時方法的執(zhí)行時間铲掐,并不想讓測試方法無限地等待時拾弃,就可以對測試方法進(jìn)行超時測試,JUnit 5 對此推出了斷言方法 assertTimeout
摆霉,提供了對超時的廣泛支持豪椿。
假設(shè)我們希望測試代碼在一秒內(nèi)執(zhí)行完畢,可以寫如下測試用例:
@Test
@DisplayName("超時方法測試")
void test_should_complete_in_one_second() {
Assertions.assertTimeoutPreemptively(Duration.of(1, ChronoUnit.SECONDS), () -> Thread.sleep(2000));
}
這個測試運行失敗携栋,因為代碼執(zhí)行將休眠兩秒鐘搭盾,而我們期望測試用例在一秒鐘之內(nèi)成功。但是如果我們把休眠時間設(shè)置一秒鐘婉支,測試仍然會出現(xiàn)偶爾失敗的情況鸯隅,這是因為測試方法執(zhí)行過程中除了目標(biāo)代碼還有額外的代碼和指令執(zhí)行會耗時,所以在超時限制上無法做到對時間參數(shù)的完全精確匹配。
異常測試:assertThrows
我們代碼中對于帶有異常的方法通常都是使用 try-catch 方式捕獲處理蝌以,針對測試這樣帶有異常拋出的代碼炕舵,而 JUnit 5 提供方法 Assertions#assertThrows(Class<T>, Executable)
來進(jìn)行測試,第一個參數(shù)為異常類型跟畅,第二個為函數(shù)式接口參數(shù)咽筋,跟 Runnable 接口相似,不需要參數(shù)徊件,也沒有返回奸攻,并且支持 Lambda表達(dá)式方式使用,具體使用方式可參考下方代碼:
@Test
@DisplayName("測試捕獲的異常")
void assertThrowsException() {
String str = null;
Assertions.assertThrows(IllegalArgumentException.class, () -> {
Integer.valueOf(str);
});
}
當(dāng)Lambda表達(dá)式中代碼出現(xiàn)的異常會跟首個參數(shù)的異常類型進(jìn)行比較虱痕,如果不屬于同一類異常舞箍,就會控制臺輸出如下類似的提示:org.opentest4j.AssertionFailedError: Unexpected exception type thrown ==> expected: <IllegalArgumentException> but was: <...Exception>
2.2.4 擴(kuò)展機(jī)制 @ExtendWith
擴(kuò)展機(jī)制與 Runner 的功能類似,在單元測試執(zhí)行的過程中實現(xiàn)一些功能皆疹。
在 SpringBoot2.0 中的@SpringBootTest
注解就標(biāo)注了 @ExtendWith({SpringExtension.class})疏橄,使單元測試伴隨著 Spring 環(huán)境(不需要 @RunWith 注解)。
為什么 SpringBoot2.0 中的
@SpringBootTest
注解中標(biāo)注了 @ExtendWith略就,但是 SpringBoot1.5 中沒有標(biāo)注 @RunWith 注解捎迫,還需要自己手動添加 @RunWith 注解?我覺得可能是1.5 的時候想著要兼容所有單元測試庫表牢,而在 2 的時候選用 junit 作為默認(rèn)窄绒,還不如直接就標(biāo)注上去。