JUnit5學習之三:Assertions類

程式設計師欣宸發表於2021-02-23

歡迎訪問我的GitHub

https://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下載所有原始碼,地址和連結資訊如下表所示:
名稱 連結 備註
專案主頁 https://github.com/zq2599/blog_demos 該專案在GitHub上的主頁
git倉庫地址(https) 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世界...
https://github.com/zq2599/blog_demos

相關文章