歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內容:所有原創文章分類彙總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;
關於《JUnit5學習》系列
《JUnit5學習》系列旨在通過實戰提升SpringBoot環境下的單元測試技能,一共八篇文章,鏈接如下:
- 基本操作
- Assumptions類
- Assertions類
- 按條件執行
- 標籤(Tag)和自定義註解
- 參數化測試(Parameterized Tests)基礎
- 參數化測試(Parameterized Tests)進階
- 綜合進階(終篇)
本篇概覽
- 本文是《JUnit5學習》系列的終篇,將JUnit5提供的一些高級特性以實戰的形式展現出來;
- JUnit5的特性非常多,《JUnit5學習》系列也只是將常用部分寫出來,未能覆蓋全部;
- 本文由以下章節組成:
- 版本設置
- 測試方法展現名稱生成器
- 重複測試
- 嵌套
- 動態測試(Dynamic Tests)
- 多線程併發執行測試方法
源碼下載
- 如果您不想編碼,可以在GitHub下載所有源碼,地址和鏈接信息如下表所示:
名稱 | 鏈接 | 備註 |
---|---|---|
項目主頁 | https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協議 |
git倉庫地址(ssh) | [email protected]:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協議 |
- 這個git項目中有多個文件夾,本章的應用在<font color="blue">junitpractice</font>文件夾下,如下圖紅框所示:
- <font color="blue">junitpractice</font>是父子結構的工程,本篇的代碼在<font color="red">advanced</font>子工程中,如下圖:
版本設置
- 《JUnit5學習》系列的代碼都在用<font color="blue">SpringBoot:2.3.4.RELEASE</font>框架,間接依賴的JUnit版本是<font color="red">5.6.2</font>;
- 本文有兩個特性要求JUnit版本達到<font color="red">5.7或者更高</font>,它們是<font color="blue">測試方法展現名稱生成器</font>和<font color="blue">動態生成測試方法</font>;
- 對於使用<font color="blue">SpringBoot:2.3.4.RELEASE</font>框架的工程,如果要指定JUnit版本,需要做以下三步操作:
- dependencyManagement節點添加junit-bom,並指定版本號:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.7.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 排除spring-boot-starter-test和junit-jupiter的間接依賴關係:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
</exclusion>
</exclusions>
</dependency>
- 添加junit-jupiter依賴,此時會使用dependencyManagement中指定的版本號:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
- 如下圖,刷新可見已經用上了<font color="blue">5.7.0</font>版本:
- 版本問題解決了,接下來正式進入進階實戰;
測試方法展現名稱生成器(Display Name Generators)
- 把<font color="red">Display Name Generators</font>翻譯成<font color="blue">測試方法展現名稱生成器</font>,可能刷新了讀者們對本文作者英文水平的認知,請您多包含...
- 先回顧一下如何指定測試方法的展現名稱,如果測試方法使用了@DisplayName,在展示單元測試執行結果時,就會顯示@DisplayName指定的字符串,如下圖所示:
3. 除了用@DisplayName指定展示名稱,JUnit5還提供了一種自動生成展示名稱的功能:@DisplayNameGeneration,來看看它是如何生成展示名稱的; 4. 演示代碼如下所示,當@DisplayNameGeneration的value設置爲<font color="blue">ReplaceUnderscores</font>時,會把方法名的所有下劃線替換爲空格:
package com.bolingcavalry.advanced.service.impl;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class ReplaceUnderscoresTest {
@Test
void if_it_is_zero() {
}
}
- 執行結果如下圖,方法<font color="blue">if_it_is_zero</font>展示出的名字爲<font color="red">if it is zero</font>:
6. 在上述替換方式的基礎上,JUnit5還提供了另一種生成展示名稱的方法:測試類名+連接符+測試方法名,並且類名和方法名的下劃線都會被替換成空格,演示代碼如下,使用了註解@IndicativeSentencesGeneration,其separator屬性就是類名和方法名之間的連接符:
package com.bolingcavalry.advanced.service.impl;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.IndicativeSentencesGeneration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@IndicativeSentencesGeneration(separator = ",測試方法:", generator = DisplayNameGenerator.ReplaceUnderscores.class)
public class IndicativeSentences_Test {
@Test
void if_it_is_one_of_the_following_years() {
}
}
- 執行結果如下:
重複測試(Repeated Tests)
- 重複測試就是指定某個測試方法反覆執行多次,演示代碼如下,可見<font color="blue">@Test</font>已被<font color="red">@RepeatedTest(5)</font>取代,數字5表示重複執行5次:
@Order(1)
@DisplayName("重複測試")
@RepeatedTest(5)
void repeatTest(TestInfo testInfo) {
log.info("測試方法 [{}]", testInfo.getTestMethod().get().getName());
}
- 執行結果如下圖:
3. 在測試方法執行時,如果想了解當前是第幾次執行,以及總共有多少次,只要給測試方法增加<font color="blue">RepetitionInfo</font>類型的入參即可,演示代碼如下,可見RepetitionInfo提供的API可以得到總數和當前次數:
@Order(2)
@DisplayName("重複測試,從入參獲取執行情況")
@RepeatedTest(5)
void repeatWithParamTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
log.info("測試方法 [{}],當前第[{}]次,共[{}]次",
testInfo.getTestMethod().get().getName(),
repetitionInfo.getCurrentRepetition(),
repetitionInfo.getTotalRepetitions());
}
- 上述代碼執行結果如下:
5. 在上圖的左下角可見,重複執行的結果被展示爲"repetition X of X"這樣的內容,其實這部分信息是可以定製的,就是RepeatedTest註解的<font color="red">name</font>屬性,演示代碼如下,可見<font color="blue">currentRepetition</font>和<font color="blue">totalRepetitions</font>是佔位符,在真正展示的時候會被分別替換成當前值和總次數:
@Order(3)
@DisplayName("重複測試,使用定製名稱")
@RepeatedTest(value = 5, name="完成度:{currentRepetition}/{totalRepetitions}")
void repeatWithCustomDisplayNameTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
log.info("測試方法 [{}],當前第[{}]次,共[{}]次",
testInfo.getTestMethod().get().getName(),
repetitionInfo.getCurrentRepetition(),
repetitionInfo.getTotalRepetitions());
}
- 上述代碼執行結果如下:
嵌套測試(Nested Tests)
- 如果一個測試類中有很多測試方法(如增刪改查,每種操作都有多個測試方法),那麼不論是管理還是結果展現都會顯得比較複雜,此時嵌套測試(Nested Tests)就派上用場了;
- 嵌套測試(Nested Tests)功能就是在測試類中創建一些內部類,以增刪改查爲例,將所有測試查找的方法放入一個內部類,將所有測試刪除的方法放入另一個內部類,再給每個內部類增加@Nested註解,這樣就會以內部類爲單位執行測試和展現結果,如下圖所示:
3. 嵌套測試的演示代碼如下:
package com.bolingcavalry.advanced.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Slf4j
@DisplayName("嵌套測試演示")
public class NestedTest {
@Nested
@DisplayName("查找服務相關的測試")
class FindService {
@Test
void findByIdTest() {}
@Test
void findByNameTest() {}
}
@Nested
@DisplayName("刪除服務相關的測試")
class DeleteService {
@Test
void deleteByIdTest() {}
@Test
void deleteByNameTest() {}
}
}
- 上述代碼執行結果如下,可見從代碼管理再到執行和結果展示,都被分組管理了:
動態測試(Dynamic Tests)
- 之前咱們寫的測試方法,主要是用<font color="blue">@Test</font>修飾,這些方法的特點就是在編譯階段就已經明確了,在運行階段也已經固定;
- JUnit5推出了另一種類型的測試方法:動態測試(Dynamic Tests),首先,測試方法是可以在運行期間被生產出來的,生產它們的地方,就是被@TestFactory修飾的方法,等到測試方法被生產出來後再像傳統的測試方法那樣被執行和結果展示;
- 下面是演示代碼,testFactoryTest方法被@TestFactory修飾,返回值是Iterable類型,裏面是多個DynamicTest實例,每個DynamicTest實例代表一個測試方法,因此,整個DynamicDemoTest類中有多少個測試方法,在編譯階段是不能確定的,只有在運行階段執行了testFactoryTest方法後,才能根據返回值確定下來:
package com.bolingcavalry.advanced.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Arrays;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
@SpringBootTest
@Slf4j
class DynamicDemoTest {
@TestFactory
Iterable<org.junit.jupiter.api.DynamicTest> testFactoryTest() {
DynamicTest firstTest = dynamicTest(
"一號動態測試用例",
() -> {
log.info("一號用例,這裏編寫單元測試邏輯代碼");
}
);
DynamicTest secondTest = dynamicTest(
"二號動態測試用例",
() -> {
log.info("二號用例,這裏編寫單元測試邏輯代碼");
}
);
return Arrays.asList(firstTest, secondTest);
}
}
- 上述代碼的執行結果如下,可見每個DynamicTest實例就相當於以前的一個@Test修飾的方法,會被執行和統計:
多線程併發執行(Parallel Execution)的介紹
- 《JUnit5學習》系列的最後,咱們來看一個既容易理解又實用的特性:多線程併發執行(Parallel Execution)
- JUnit5中的併發執行測試可以分爲以下三種場景:
- 多個測試類,它們各自的測試方法同時執行;
- 一個測試類,裏面的多個測試方法同時執行;
- 一個測試類,裏面的一個測試方法,在重複測試(Repeated Tests)或者參數化測試(Parameterized Tests)的時候,這個測試方法被多個線程同時執行;
多線程併發執行(Parallel Execution)實戰
- 前面介紹了多線程併發執行有三種場景,文章篇幅所限就不逐個編碼實戰了,就選擇第三種場景來實踐吧,即:一個測試類裏面的一個測試方法,在重複測試時多線程併發執行,至於其他兩種場景如何設置,接下來的文中也會講清楚,您自行實踐即可;
- 首先是創建JUnit5的配置文件,如下圖,在<font color="blue">test</font>文件夾上點擊鼠標右鍵,在彈出的菜單選擇"New"->"Directory":
- 彈出的窗口如下圖,雙擊紅框位置的"resources",即可新建resources目錄:
- 在新增的resources目錄中新建文件<font color="blue">junit-platform.properties</font>,內容如下,每個配置項都有詳細的說明:
# 並行開關true/false
junit.jupiter.execution.parallel.enabled=true
# 方法級多線程開關 same_thread/concurrent
junit.jupiter.execution.parallel.mode.default = same_thread
# 類級多線程開關 same_thread/concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread
# 併發策略有以下三種可選:
# fixed:固定線程數,此時還要通過junit.jupiter.execution.parallel.config.fixed.parallelism指定線程數
# dynamic:表示根據處理器和核數計算線程數
# custom:自定義併發策略,通過這個配置來指定:junit.jupiter.execution.parallel.config.custom.class
junit.jupiter.execution.parallel.config.strategy = fixed
# 併發線程數,該配置項只有當併發策略爲fixed的時候纔有用
junit.jupiter.execution.parallel.config.fixed.parallelism = 5
- 由於實踐的是同一個類同一個方法多次執行的併發,因此上述配置中,類級多線程開關和方法級多線程開關都選擇了"同一個線程",也就是說不需要併發執行多個類或者多個方法,請您根據自己的需求自行調整;
- 關於併發策略,這裏選擇的是<font color="blue">動態調整</font>,我這裏是<font color="blue">i5-8400</font>處理器,擁有六核心六線程,稍後咱們看看執行效果與這個硬件配置是否有關係;
- 接下來編寫測試代碼,先寫一個單線程執行的,可見@Execution的值爲<font color="red">SAME_THREAD</font>,限制了重複測試時在同一個線程內順序執行:
package com.bolingcavalry.advanced.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ParallelExecutionTest {
@Order(1)
@Execution(ExecutionMode.SAME_THREAD)
@DisplayName("單線程執行10次")
@RepeatedTest(value = 10, name="完成度:{currentRepetition}/{totalRepetitions}")
void sameThreadTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
log.info("測試方法 [{}],當前第[{}]次,共[{}]次",
testInfo.getTestMethod().get().getName(),
repetitionInfo.getCurrentRepetition(),
repetitionInfo.getTotalRepetitions());
}
}
- 執行結果如下,可見確實是單線程:
- 重複測試時併發執行的代碼如下,@Execution的值爲<font color="red">CONCURRENT</font>:
@Order(2)
@Execution(ExecutionMode.CONCURRENT)
@DisplayName("多線程執行10次")
@RepeatedTest(value = 10, name="完成度:{currentRepetition}/{totalRepetitions}")
void concurrentTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
log.info("測試方法 [{}],當前第[{}]次,共[{}]次",
testInfo.getTestMethod().get().getName(),
repetitionInfo.getCurrentRepetition(),
repetitionInfo.getTotalRepetitions());
}
- 執行結果如下,從紅框1可見順序已經亂了,從紅框2可見十次測試方法是在五個線程中執行的:
11. 最後是參數化測試的演示,也可以設置爲多線程並行執行:
@Order(3)
@Execution(ExecutionMode.CONCURRENT)
@DisplayName("多個int型入參")
@ParameterizedTest
@ValueSource(ints = { 1,2,3,4,5,6,7,8,9,0 })
void intsTest(int candidate) {
log.info("ints [{}]", candidate);
}
- 執行結果如下圖,可見也是5個線程並行執行的:
結束語
至此,《JUnit5學習》系列已經全部完成,感謝您的耐心閱讀,希望這個原創系列能夠帶給您一些有用的信息,爲您的單元測試提供一些參考,如果發現文章有錯誤,期待您能指點一二;
你不孤單,欣宸原創一路相伴
歡迎關注公衆號:程序員欣宸
微信搜索「程序員欣宸」,我是欣宸,期待與您一同暢遊Java世界... https://github.com/zq2599/blog_demos