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


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

image

大體流程如下:

image-20210825134526673

圖來源:深入JUnit源碼之Runner

SpringRunner

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

image

TestContextManager

由于市面上有多種單元測(cè)試框架,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 提供的各類可以在單元測(cè)試中使用的注解。

image

二、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è)試用例。
image

2.2.2 新特性

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

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

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

  4. 提供對(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)日志如下:

image

可以看到左邊一欄的結(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>

image

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

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

image

這是基本的用法,我們還可以對(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è)試");
}
image

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

image-20210906164343268

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容