JUnit5學習之三:Assertions類
- 2021 年 2 月 23 日
- 筆記
歡迎訪問我的GitHub
//github.com/zq2599/blog_demos
內容:所有原創文章分類匯總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;
關於《JUnit5學習》系列
《JUnit5學習》系列旨在通過實戰提升SpringBoot環境下的單元測試技能,一共八篇文章,鏈接如下:
- 基本操作
- Assumptions類
- Assertions類
- 按條件執行
- 標籤(Tag)和自定義註解
- 參數化測試(Parameterized Tests)基礎
- 參數化測試(Parameterized Tests)進階
- 綜合進階(終篇)
本篇概覽
本文是《JUnit5學習》系列的第三篇,主要是學習Assertions類(org.junit.jupiter.api.Assertions),Assertions類的一系列靜態方法給我們提供了單元測試時常用的斷言功能,本篇主要內容如下:
- Assertions源碼分析
- 寫一段代碼,使用Assertions的常用靜態方法
- 使用異常斷言
- 使用超時斷言
- 了解第三方斷言庫
源碼下載
- 如果您不想編碼,可以在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協議 |
- 這個git項目中有多個文件夾,本章的應用在junitpractice文件夾下,如下圖紅框所示:
- junitpractice是父子結構的工程,本篇的代碼在assertassume子工程中,如下圖:
Assertions源碼分析
- 下圖是一段最簡單最常見的單元測試代碼,也就是Assertions.assertEquals方法,及其執行效果:
- 將Assertions.assertEquals方法逐層展開,如下圖所示,可見入參expected和actual的值如果不相等,就會在AssertionUtils.fail方法中拋出AssertionFailedError異常:
- 用類圖工具查看Assertions類的方法,如下圖,大部分是與assertEquals方法類似的判斷,例如對象是否為空,數組是否相等,判斷失敗都會拋出AssertionFailedError異常:
4. 判斷兩個數組是否相等的邏輯與判斷兩個對象略有不同,可以重點看看,方法源碼如下:
public static void assertArrayEquals(Object[] expected, Object[] actual) {
AssertArrayEquals.assertArrayEquals(expected, actual);
}
- 將上述代碼逐層展開,在AssertArrayEquals.java中見到了完整的數組比較邏輯,如下圖:
- 接下來,咱們編寫一些單元測試代碼,把Assertions類常用的方法都熟悉一遍;
編碼實戰
- 打開junitpractice工程的子工程assertassume,新建測試類AssertionsTest.java:
2. 最簡單的判斷,兩個入參相等就不拋異常(AssertionFailedError):
@Test
@DisplayName("最普通的判斷")
void standardTest() {
assertEquals(2, Math.addExact(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));
}
- assertAll方法可以將多個判斷邏輯放在一起處理,只要有一個報錯就會導致整體測試不通過,並且執行結果中會給出具體的失敗詳情:
@Test
@DisplayName("批量判斷(必須全部通過,否則就算失敗)")
void groupedAssertions() {
// 將多個判斷放在一起執行,只有全部通過才算通過,如果有未通過的,會有對應的提示
assertAll("單個測試方法中多個判斷",
() -> assertEquals(1, 1),
() -> assertEquals(2, 1),
() -> assertEquals(3, 1)
);
}
上述代碼執行結果如下:
異常斷言
- Assertions.assertThrows方法,用來測試Executable實例執行execute方法時是否拋出指定類型的異常;
- 如果execute方法執行時不拋出異常,或者拋出的異常與期望類型不一致,都會導致測試失敗;
- 寫段代碼驗證一下,如下,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的常規用法,接下來要重點關注的就是和超時相關的測試方法;
超時相關的測試
- 超時測試的主要目標是驗證指定代碼能否在規定時間內執行完,最常用的assertTimeout方法內部實現如下圖,可見被測試的代碼通過ThrowingSupplier實例傳入,被執行後再檢查耗時是否超過規定時間,超過就調用fail方法拋AssertionFailedError異常:
- 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);
}
- 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);
}
});
}
- 來看看執行結果,如下圖,通過日誌可見,Executable的execute方法是在新的線程執行的,並且被中斷了,提前完成單元測試,測試結果是不通過:
第三方斷言庫
- 除了junit的Assertions類,還可以選擇第三方庫提供的斷言能力,比較典型的有AssertJ, Hamcrest, Truth這三種,它們都有各自的特色和適用場景,例如Hamcrest的特點是匹配器(matchers ),而Truth來自谷歌的Guava團隊,編寫的代碼是鏈式調用風格,簡單易讀,斷言類型相對更少卻不失功能;
- springboot默認依賴了hamcrest庫,依賴關係如下圖:
- 一個簡單的基於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)));
}
}
- 執行結果如下:
- 以上就是JUnit5常用的斷言功能,希望本篇能助您夯實基礎,為後續寫出更合適的用例做好準備;
你不孤單,欣宸原創一路相伴
歡迎關注公眾號:程序員欣宸
微信搜索「程序員欣宸」,我是欣宸,期待與您一同暢遊Java世界…
//github.com/zq2599/blog_demos