單元測試框架 JUnit 進(jìn)階指南


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í)行流程:

image

大體流程如下:

image-20210825134526673

圖來源:深入JUnit源碼之Runner

SpringRunner

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

image

TestContextManager

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

image-20210916100423253

而紅框中的那些功能又是委托給了 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 提供的各類可以在單元測試中使用的注解窘面。

image

二翠语、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 的測試用例憨募。
image

2.2.2 新特性

  1. 提供全新的斷言和測試注解紧索,支持測試類內(nèi)嵌

  2. 更豐富的測試方式:支持動態(tài)測試,重復(fù)測試菜谣,參數(shù)化測試等

  3. 實現(xiàn)了模塊化珠漂,讓測試執(zhí)行和測試發(fā)現(xiàn)等不同模塊解耦,減少依賴

  4. 提供對 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("我的第二個測試開始測試");
    }
}

直接運行這個測試用例仰泻,可以看到控制臺日志如下:

image

可以看到左邊一欄的結(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í)行轴术,只有單獨的方法信息打印:

image

@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é)果:

image
重復(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 的運行效果如下圖所示:

image

這是基本的用法框产,我們還可以對重復(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í)行測試");
}
image

@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)一些功能皆疹。

image-20210906164343268

在 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)注上去。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末崔兴,一起剝皮案震驚了整個濱河市彰导,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌敲茄,老刑警劉巖位谋,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異堰燎,居然都是意外死亡掏父,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門秆剪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赊淑,“玉大人,你說我怎么就攤上這事仅讽√杖保” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵洁灵,是天一觀的道長饱岸。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么伶贰? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任蛛砰,我火速辦了婚禮罐栈,結(jié)果婚禮上黍衙,老公的妹妹穿的比我還像新娘。我一直安慰自己荠诬,他們只是感情好琅翻,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著柑贞,像睡著了一般方椎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上钧嘶,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天棠众,我揣著相機(jī)與錄音,去河邊找鬼有决。 笑死闸拿,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的书幕。 我是一名探鬼主播新荤,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼台汇!你這毒婦竟也來了苛骨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤苟呐,失蹤者是張志新(化名)和其女友劉穎痒芝,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牵素,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡吼野,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了两波。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞳步。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖腰奋,靈堂內(nèi)的尸體忽然破棺而出单起,到底是詐尸還是另有隱情,我是刑警寧澤劣坊,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布嘀倒,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏测蘑。R本人自食惡果不足惜灌危,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碳胳。 院中可真熱鬧勇蝙,春花似錦、人聲如沸挨约。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诫惭。三九已至翁锡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間夕土,已是汗流浹背馆衔。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留怨绣,地道東北人角溃。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像梨熙,于是被迫代替她去往敵國和親开镣。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容