歡迎訪問我的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) | git@github.com: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 = 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 = 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 = 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