歡迎訪問我的GitHub
https://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下載所有原始碼,地址和連結資訊如下表所示:
名稱 | 連結 | 備註 |
---|---|---|
專案主頁 | 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協議 |
- 這個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世界...
https://github.com/zq2599/blog_demos