歡迎訪問我的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專案中有多個檔案夾,本章的應用在junitpractice檔案夾下,如下圖紅框所示:

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

版本設定
- 《JUnit5學習》系列的代碼都在用SpringBoot:2.3.4.RELEASE框架,間接依賴的JUnit版本是5.6.2;
- 本文有兩個特性要求JUnit版本達到5.7或者更高,它們是測驗方法展現名稱生成器和動態生成測驗方法;
- 對于使用SpringBoot:2.3.4.RELEASE框架的工程,如果要指定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>
- 如下圖,重繪可見已經用上了5.7.0版本:

- 版本問題解決了,接下來正式進入進階實戰;
測驗方法展現名稱生成器(Display Name Generators)
- 把Display Name Generators翻譯成測驗方法展現名稱生成器,可能重繪了讀者們對本文作者英文水平的認知,請您多包含...
- 先回顧一下如何指定測驗方法的展現名稱,如果測驗方法使用了@DisplayName,在展示單元測驗執行結果時,就會顯示@DisplayName指定的字串,如下圖所示:

3. 除了用@DisplayName指定展示名稱,JUnit5還提供了一種自動生成展示名稱的功能:@DisplayNameGeneration,來看看它是如何生成展示名稱的;
4. 演示代碼如下所示,當@DisplayNameGeneration的value設定為ReplaceUnderscores時,會把方法名的所有下劃線替換為空格:
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() {
}
}
- 執行結果如下圖,方法if_it_is_zero展示出的名字為if it is zero:

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)
- 重復測驗就是指定某個測驗方法反復執行多次,演示代碼如下,可見@Test已被@RepeatedTest(5)取代,數字5表示重復執行5次:
@Order(1)
@DisplayName("重復測驗")
@RepeatedTest(5)
void repeatTest(TestInfo testInfo) {
log.info("測驗方法 [{}]", testInfo.getTestMethod().get().getName());
}
- 執行結果如下圖:

3. 在測驗方法執行時,如果想了解當前是第幾次執行,以及總共有多少次,只要給測驗方法增加RepetitionInfo型別的入參即可,演示代碼如下,可見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注解的name屬性,演示代碼如下,可見currentRepetition和totalRepetitions是占位符,在真正展示的時候會被分別替換成當前值和總次數:
@Order(3)
@DisplayName("重復測驗,使用定制名稱")
@RepeatedTest(value = https://www.cnblogs.com/bolingcavalry/p/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)
- 之前咱們寫的測驗方法,主要是用@Test修飾,這些方法的特點就是在編譯階段就已經明確了,在運行階段也已經固定;
- 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的組態檔,如下圖,在test檔案夾上點擊滑鼠右鍵,在彈出的選單選擇"New"->"Directory":

- 彈出的視窗如下圖,雙擊紅框位置的"resources",即可新建resources目錄:

- 在新增的resources目錄中新建檔案junit-platform.properties,內容如下,每個配置項都有詳細的說明:
# 并行開關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
- 由于實踐的是同一個類同一個方法多次執行的并發,因此上述配置中,類級多執行緒開關和方法級多執行緒開關都選擇了"同一個執行緒",也就是說不需要并發執行多個類或者多個方法,請您根據自己的需求自行調整;
- 關于并發策略,這里選擇的是動態調整,我這里是i5-8400處理器,擁有六核心六執行緒,稍后咱們看看執行效果與這個硬體配置是否有關系;
- 接下來撰寫測驗代碼,先寫一個單執行緒執行的,可見@Execution的值為SAME_THREAD,限制了重復測驗時在同一個執行緒內順序執行:
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 = https://www.cnblogs.com/bolingcavalry/p/10, name="完成度:{currentRepetition}/{totalRepetitions}")
void sameThreadTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
log.info("測驗方法 [{}],當前第[{}]次,共[{}]次",
testInfo.getTestMethod().get().getName(),
repetitionInfo.getCurrentRepetition(),
repetitionInfo.getTotalRepetitions());
}
}
- 執行結果如下,可見確實是單執行緒:

- 重復測驗時并發執行的代碼如下,@Execution的值為CONCURRENT:
@Order(2)
@Execution(ExecutionMode.CONCURRENT)
@DisplayName("多執行緒執行10次")
@RepeatedTest(value = https://www.cnblogs.com/bolingcavalry/p/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系列
- Spring系列
- Docker系列
- kubernetes系列
- 資料庫+中間件系列
- DevOps系列
歡迎關注公眾號:程式員欣宸
微信搜索「程式員欣宸」,我是欣宸,期待與您一同暢游Java世界...
https://github.com/zq2599/blog_demos
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/265020.html
標籤:Java
