JUnit5學習之三:Assertions類

  • 2021 年 2 月 23 日
  • 筆記

歡迎訪問我的GitHub

//github.com/zq2599/blog_demos

內容:所有原創文章分類匯總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;

關於《JUnit5學習》系列

《JUnit5學習》系列旨在通過實戰提升SpringBoot環境下的單元測試技能,一共八篇文章,鏈接如下:

  1. 基本操作
  2. Assumptions類
  3. Assertions類
  4. 按條件執行
  5. 標籤(Tag)和自定義註解
  6. 參數化測試(Parameterized Tests)基礎
  7. 參數化測試(Parameterized Tests)進階
  8. 綜合進階(終篇)

本篇概覽

本文是《JUnit5學習》系列的第三篇,主要是學習Assertions類(org.junit.jupiter.api.Assertions),Assertions類的一系列靜態方法給我們提供了單元測試時常用的斷言功能,本篇主要內容如下:

  1. Assertions源碼分析
  2. 寫一段代碼,使用Assertions的常用靜態方法
  3. 使用異常斷言
  4. 使用超時斷言
  5. 了解第三方斷言庫

源碼下載

  1. 如果您不想編碼,可以在GitHub下載所有源碼,地址和鏈接信息如下表所示:
名稱 鏈接 備註
項目主頁 //github.com/zq2599/blog_demos 該項目在GitHub上的主頁
git倉庫地址(https) //github.com/zq2599/blog_demos.git 該項目源碼的倉庫地址,https協議
git倉庫地址(ssh) git@github.com:zq2599/blog_demos.git 該項目源碼的倉庫地址,ssh協議
  1. 這個git項目中有多個文件夾,本章的應用在junitpractice文件夾下,如下圖紅框所示:

  1. junitpractice是父子結構的工程,本篇的代碼在assertassume子工程中,如下圖:

Assertions源碼分析

  1. 下圖是一段最簡單最常見的單元測試代碼,也就是Assertions.assertEquals方法,及其執行效果:

  1. 將Assertions.assertEquals方法逐層展開,如下圖所示,可見入參expected和actual的值如果不相等,就會在AssertionUtils.fail方法中拋出AssertionFailedError異常:

  1. 用類圖工具查看Assertions類的方法,如下圖,大部分是與assertEquals方法類似的判斷,例如對象是否為空,數組是否相等,判斷失敗都會拋出AssertionFailedError異常:


4. 判斷兩個數組是否相等的邏輯與判斷兩個對象略有不同,可以重點看看,方法源碼如下:

	public static void assertArrayEquals(Object[] expected, Object[] actual) {
		AssertArrayEquals.assertArrayEquals(expected, actual);
	}
  1. 將上述代碼逐層展開,在AssertArrayEquals.java中見到了完整的數組比較邏輯,如下圖:

  • 接下來,咱們編寫一些單元測試代碼,把Assertions類常用的方法都熟悉一遍;

編碼實戰

  1. 打開junitpractice工程的子工程assertassume,新建測試類AssertionsTest.java:


2. 最簡單的判斷,兩個入參相等就不拋異常(AssertionFailedError):

    @Test
    @DisplayName("最普通的判斷")
    void standardTest() {
       assertEquals(2, Math.addExact(1, 1));
    }
  1. 還有另一個assertEquals方法,能接受Supplier類型的入參,當判斷不通過時才會調用Supplier.get方法獲取字符串作為失敗提示消息(如果測試通過則Supplier.get方法不會被執行):
    @Test
    @DisplayName("帶失敗提示的判斷(拼接消息字符串的代碼只有判斷失敗時才執行)")
    void assertWithLazilyRetrievedMessage() {
        int expected = 2;
        int actual = 1;

        assertEquals(expected,
                actual,
                // 這個lambda表達式,只有在expected和actual不相等時才執行
                ()->String.format("期望值[%d],實際值[%d]", expected, actual));
    }
  1. assertAll方法可以將多個判斷邏輯放在一起處理,只要有一個報錯就會導致整體測試不通過,並且執行結果中會給出具體的失敗詳情:
    @Test
    @DisplayName("批量判斷(必須全部通過,否則就算失敗)")
    void groupedAssertions() {
        // 將多個判斷放在一起執行,只有全部通過才算通過,如果有未通過的,會有對應的提示
        assertAll("單個測試方法中多個判斷",
                () -> assertEquals(1, 1),
                () -> assertEquals(2, 1),
                () -> assertEquals(3, 1)
        );
    }

上述代碼執行結果如下:

異常斷言

  1. Assertions.assertThrows方法,用來測試Executable實例執行execute方法時是否拋出指定類型的異常;
  2. 如果execute方法執行時不拋出異常,或者拋出的異常與期望類型不一致,都會導致測試失敗;
  3. 寫段代碼驗證一下,如下,1除以0會拋出ArithmeticException異常,符合assertThrows指定的異常類型,因此測試可以通過:
    @Test
    @DisplayName("判斷拋出的異常是否是指定類型")
    void exceptionTesting() {

        // assertThrows的第二個參數是Executable,
        // 其execute方法執行時,如果拋出了異常,並且異常的類型是assertThrows的第一個參數(這裡是ArithmeticException.class),
        // 那麼測試就通過了,返回值是異常的實例
        Exception exception = assertThrows(ArithmeticException.class, () -> Math.floorDiv(1,0));

        log.info("assertThrows通過後,返回的異常實例:{}", exception.getMessage());
    }
  • 以上是Assertions的常規用法,接下來要重點關注的就是和超時相關的測試方法;

超時相關的測試

  1. 超時測試的主要目標是驗證指定代碼能否在規定時間內執行完,最常用的assertTimeout方法內部實現如下圖,可見被測試的代碼通過ThrowingSupplier實例傳入,被執行後再檢查耗時是否超過規定時間,超過就調用fail方法拋AssertionFailedError異常:

  1. assertTimeout的用法如下,期望時間是1秒,實際上Executable實例的execute用了兩秒才完成,因此測試失敗:
    @Test
    @DisplayName("在指定時間內完成測試")
    void timeoutExceeded() {
        // 指定時間是1秒,實際執行用了2秒
        assertTimeout(ofSeconds(1), () -> {
            try{
              Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

執行結果如下圖:


3. 上面的演示中,assertTimeout的第二個入參類型是Executable,此外還有另一個assertTimeout方法,其第二個入參是ThrowingSupplier類型,該類型入參的get方法必須要有返回值,假設是XXX,而assertTimeout就拿這個XXX作為它自己的返回值,使用方法如下:

    @Test
    @DisplayName("在指定時間內完成測試")
    void timeoutNotExceededWithResult() {

        // 準備ThrowingSupplier類型的實例,
        // 裏面的get方法sleep了1秒鐘,然後返回一個字符串
        ThrowingSupplier<String> supplier = () -> {

            try{
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return "我是ThrowingSupplier的get方法的返回值";
        };

        // 指定時間是2秒,實際上ThrowingSupplier的get方法只用了1秒
        String actualResult = assertTimeout(ofSeconds(2), supplier);

        log.info("assertTimeout的返回值:{}", actualResult);
    }

上述代碼執行結果如下,測試通過並且ThrowingSupplier實例的get方法的返回值也被打印出來:


4. 剛才咱們看過了assertTimeout的內部實現代碼,是將入參Executable的execute方法執行完成後,再檢查execute方法的耗時是否超過預期,這種方法的弊端是必須等待execute方法執行完成才知道是否超時,assertTimeoutPreemptively方法也是用來檢測代碼執行是否超時的,但是避免了assertTimeout的必須等待execute執行完成的弊端,避免的方法是用一個新的線程來執行execute方法,下面是assertTimeoutPreemptively的源碼:

public static void assertTimeoutPreemptively(Duration timeout, Executable executable) {
	AssertTimeout.assertTimeoutPreemptively(timeout, executable);
}
  1. assertTimeoutPreemptively方法的Executable入參,其execute方法會在一個新的線程執行,假設是XXX線程,當等待時間超過入參timeout的值時,XXX線程就會被中斷,並且測試結果是失敗,下面是assertTimeoutPreemptively的用法演示,設置的超時時間是2秒,而Executable實例的execute卻sleep了10秒:
    @Test
    void timeoutExceededWithPreemptiveTermination() {
        log.info("開始timeoutExceededWithPreemptiveTermination");
        assertTimeoutPreemptively(ofSeconds(2), () -> {
            log.info("開始sleep");
            try{
                Thread.sleep(10000);
                log.info("sleep了10秒");
            } catch (InterruptedException e) {
                log.error("線程sleep被中斷了", e);
            }
        });
    }
  1. 來看看執行結果,如下圖,通過日誌可見,Executable的execute方法是在新的線程執行的,並且被中斷了,提前完成單元測試,測試結果是不通過:

第三方斷言庫

  1. 除了junit的Assertions類,還可以選擇第三方庫提供的斷言能力,比較典型的有AssertJ, Hamcrest, Truth這三種,它們都有各自的特色和適用場景,例如Hamcrest的特點是匹配器(matchers ),而Truth來自谷歌的Guava團隊,編寫的代碼是鏈式調用風格,簡單易讀,斷言類型相對更少卻不失功能;
  2. springboot默認依賴了hamcrest庫,依賴關係如下圖:

  1. 一個簡單的基於hamcrest的匹配器的單元測試代碼如下,由於預期和實際的值不相等,因此會匹配失敗:
package com.bolingcavalry.assertassume.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

@SpringBootTest
@Slf4j
public class HamcrestTest {

    @Test
    @DisplayName("體驗hamcrest")
    void assertWithHamcrestMatcher() {
        assertThat(Math.addExact(1, 2), is(equalTo(5)));
    }
}
  1. 執行結果如下:

  • 以上就是JUnit5常用的斷言功能,希望本篇能助您夯實基礎,為後續寫出更合適的用例做好準備;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 數據庫+中間件系列
  6. DevOps系列

歡迎關注公眾號:程序員欣宸

微信搜索「程序員欣宸」,我是欣宸,期待與您一同暢遊Java世界…
//github.com/zq2599/blog_demos

Exit mobile version