聊聊 Java 的單元測試
單元測試框架
Java 中,JUnit
和 TestNG
是最受歡迎的單元測試框架。
- JUnit
- TestNG
JUnit
首先是大名鼎鼎的 JUnit ,JUnit 已經成為 Java 應用程式單元測試的事實標準。
JUnit 是一個開源的 Java 語言的單元測試框架,專門針對 Java 設計,使用最廣泛。JUnit 目前最新版本是 5
JUnit5 的組成:JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit5 建議使用 Java8 及以上版本
- JUnit Platform 是在 JVM 上啟動測試框架的基礎,它定義了
TestEngine
在平臺執行的新測試框架的 API - JUnit Jupiter 它用於編寫測試程式碼的新的程式設計和擴充套件模型。它具有所有新的 Junit 註釋和
TestEngine
實現來執行這些註釋編寫的測試。 - JUnit Vintage JUnit4 已經存在了很長時間,並且有許多以 JUnit4 編寫的測試。JUnit Jupiter 還需要支援這些測試。為此,開發了 JUnit Vintage 子專案。提供了一個測試引擎,用於在平臺上執行基於 JUnit 3 和 JUnit 4 的測試。它要求 JUnit 4.12 或更高版本出現在類路徑或模組路徑中。從它的名字 Vintage(古老的;古色古香的)中也能有所體會。
簡單例子
我們先來個最簡單的例子,別看簡單,很多人會犯錯
Java
@SpringBootTest
@RunWith(SpringRunner.class)
public class JunitTest {
@Test
public void testJunit(){
System.out.println("junit test");
}
}
很簡單對吧,如果你用了 SpringBoot 簡單到好像沒啥說的,其實不然,我們來聊聊:
首先,這段程式碼使用的是 JUnit 4 還是 JUnit5 ? 你可能會覺得,4 和 5 沒啥區別吧,用哪個不一樣嗎? 程式碼能跑不就行了?
不是的,4 和 5 肯定有區別這個不用我說了。能跑沒問題,但如果你不管是 4 還是 5 都認為一樣,API 混用,甚至亂用,那這時候測試出現的各種報錯,導致你很懵逼,而且不知道為什麼,一通亂查也不知所然。
上面這段程式碼其實是 JUnit 4 版本,我們看一下 import 就一目瞭然了,然而可能你在開發的時候沒太注意這裡是 4 還是 5
Java
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
這裡確定了,使用的是 4 的版本,這裡有幾個要注意的點:
@Test
的包是org.junit.Test
,不要搞錯了,因為有好幾個同名包- 需要
@RunWith(SpringRunner.class)
- 測試類和測試方法需要
public
修飾
我們看下完整的例子:
```Java import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest @RunWith(SpringRunner.class) public class JunitTest { @Test public void testJunit(){ System.out.println("junit test "); } }
```
這裡強調下環境 ,springboot2.2.x 之前支援 JUnit 4
上面有一點提到了 需要 public
修飾的問題,這不很正常嗎,為什麼要強調?
那是因為 JUnit 5 不需要了,我們看一下用 JUnit 5 來實現的同樣的例子 (SpringBoot 2.2.x 之後支援 JUnit 5):
```Java import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest class JunitTest { @Test void testJunit5(){ System.out.println("junit5"); } } ```
這麼簡單嗎? 對,就是這麼簡單,所以我說 4 和 5 不一樣。我們來看區別的地方:
@Test
的包是org.junit.jupiter.api.Test
- 不需要
@RunWith(SpringRunner.class)
- 測試類和測試方法不需要
public
修飾
我見過很多同學在寫測試用例時出現的所謂詭異問題,都是因為他自己都沒搞清楚用的是 4 還是 5 的情況下將 4 和 5 混用導致的。
如果你的測試用例是 4 ,可以遷移到 5 了,有關 JUnit 4 遷移到 JUnit5 的話題可以參考這篇文章 ,通過工具可能節省很多時間:http://blog.jetbrains.com/idea/2020/08/migrating-from-junit-4-to-junit-5/
我們再來看一下 pom 依賴這裡,你是不是經常看到有關 test 的依賴是這樣寫的:
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
為啥? 為什麼要排除 junit-vintage-engine ?如果你認真閱讀了前文,你應該能猜到為什麼了。
JUnit Vintage 是為了相容 3 和 4 的一個 engine,如果我們的測試程式碼都用 5 實現,不需要相容 3 和 4 ,那要它幹嘛? 當然是幹掉呀,哈哈。
但如果你需要相容,那請不要那麼魯莽。上面的這段 dependency 主要用於 spring-boot-starter-test 的 2.2.x 和 2.3.x 版本中。spring-boot-starter-test 2.4.x 版本中,已經不再包含 junit-vintage-engine 這個依賴項了
常規套路
|Annotations|描述|
|-|-|
|@BeforeEach
|在方法上註解,在每個測試方法執行之前執行。|
|@AfterEach
|在方法上註解,在每個測試方法執行之後執行|
|@BeforeAll
|該註解方法會在所有測試方法之前執行,該方法必須是靜態的。|
|@AfterAll
|該註解方法會在所有測試方法之後執行,該方法必須是靜態的。|
|@Test
|用於將方法標記為測試方法|
|@DisplayName
|用於為測試類或測試方法提供任何自定義顯示名稱|
|@Disable
|用於禁用或忽略測試類或方法|
|@Nested
|用於建立巢狀測試類|
|@Tag
|用於測試發現或過濾的標籤來標記測試方法或類|
|@TestFactory
|標記一種方法是動態測試的測試工場|
常規套路不說了,比較簡單,一看就明白,說幾個有意思的。
重複性測試
```Java @RepeatedTest(5) void repeatTest(TestInfo testInfo,RepetitionInfo repetitionInfo){
System.out.println("repeat:" + testInfo.getDisplayName());
System.out.println("這是第 "+ repetitionInfo.getCurrentRepetition()+ "次重複");
} ```
不用自己寫 for 迴圈了,人家自己帶重複的註解,上面兩個變數也是自己帶的,方便拿到重複資訊。
基於引數測試
Java
@ParameterizedTest
@ValueSource(strings = {"java", "python", "go"})
void containsChar(String candidate) {
assertTrue(candidate.contains("o"));
}
如果你的引數少,也不用寫迴圈了,直接寫註解裡,還挺方便的。
超時測試
Java
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds500Milliseconds() {
// fails if execution time exceeds 500 milliseconds
}
可以設定 超時的單位和時長
在 assert 中也可以測超時,可以這樣寫:
```Java // timed out after 5 seconds @Test void test_timeout_fail() { // assertTimeout(Duration.ofSeconds(5), () -> delaySecond(10)); // this will fail
assertTimeout(Duration.ofSeconds(5), () -> delaySecond(1)); // pass
}
void delaySecond(int second) {
try {
TimeUnit.SECONDS.sleep(second);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
並行測試
以上測試用例都是用主執行緒或者單執行緒跑的,下面我們玩兒個多執行緒並行 test
首先你要在你的 classpath 下面建一個檔案 junit-platform.properties
接著加兩行配置
```.properties junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent
```
行了,再跑你的用例就是多執行緒並行執行的了,當然如果用例本來就設計成單執行緒的看不出來,那可以使用 Repeat 試一下,比如上面講過的這個:
```Java @RepeatedTest(5) void repeatTest(TestInfo testInfo,RepetitionInfo repetitionInfo){
System.out.println("repeat:" + testInfo.getDisplayName());
System.out.println("這是第 "+ repetitionInfo.getCurrentRepetition()+ "次重複");
} ```
上面這個是對一個方法的重複執行並行,有時候我們是想讓一個類中的多個方法並行,能不能做到? 可以,改下配置就好了
.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread
如果反過來呢? 多個類並行,類中的方法序列 也可以,還是改配置:
.properties
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
MockMVC
你測個 service
測個 dao
很簡單,把 Bean 注入就可以了,Controller
怎麼測? 我們要利用下 MockMVC 了
MockMvc 實現了對 Http 請求的模擬,能夠直接使用網路的形式,轉換到 Controller 的呼叫,這樣可以使得測試速度快、不依賴網路環境,而且提供了一套驗證的工具,這樣可以使得請求的驗證統一而且很方便。
我們先看一個簡單的例子:
```Java import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.web.servlet.MockMvc; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest @AutoConfigureMockMvc class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private HelloController helloController;
@Test
public void shouldReturnDefaultMessage() throws Exception {
this.mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(containsString("Hello World")));
}
} ```
解釋下沒見過的註解:
@AutoConfigureMockMvc
:用於自動配置 MockMvc, 配置後 MockMvc 類可以直接注入
此外我們利用 @Autowired 注入了一個 MockMvc 的 Bean 例項。我們通過這個例子來模擬請求 /hello
這個 Controller 資源,並且通過判斷返回的 content 內容是否包含 Hello World
字串來決定這個用例的執行是否成功。
注意 imports 部分,我們匯入了 MockMvcRequestBuilders 的一些靜態方法。整個方法就一行程式碼,解釋一下:
- perform : 執行一個請求
- andDo : 新增一個結果處理器,表示要對結果做點什麼事情,比如此處使用 print():輸出整個響應結果資訊
- andExpect : 新增執行完成後的斷言
我們看下執行結果:
```text MockHttpServletRequest: HTTP Method = GET Request URI = /hello Parameters = {} Headers = [] Body = null Session Attrs = {}
Handler: Type = com.xiaobox.springbootdemo.controller.HelloController Method = com.xiaobox.springbootdemo.controller.HelloController#hello(String)
Async: Async started = false Async result = null
Resolved Exception: Type = null
ModelAndView: View name = null View = null Model = null
FlashMap: Attributes = null
MockHttpServletResponse: Status = 200 Error message = null Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"12"] Content type = text/plain;charset=UTF-8 Body = Hello World! Forwarded URL = null Redirected URL = null Cookies = [] ```
我們來看下一個例子
```Java
@WebMvcTest class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private HelloController helloController;
@Test
public void shouldReturnDefaultMessage() throws Exception {
this.mockMvc.perform(get("/hello"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(containsString("Hello World")));
}
} ```
你發現我們只是把 class 頭上的註解換成了 @WebMvcTest,其實的沒變,是的。但卻比上一段程式碼快 3 倍。為什麼?
因為之前的寫法會把 Spring 完整的應用上下文全啟動了,而 @WebMvcTest 是將測試範圍縮小到僅啟動 web 層,所以會快。當你只想測試 http 到 controller 這層的時候,可以用 @WebMvcTest 註解。
你甚至還可以告訴框架只啟動某一個 controller 這樣更快,比如:@WebMvcTest(HomeController.class)
上面是 WebMvcTest 的第一個場景, 我們來看第二個場景:也是測 controller ,但 controller 呼叫的 service 我們也 mock,不走真正 service 程式碼邏輯。這在有時你的 service 沒準備好,或者不方便直接呼叫時會很有用。
```Java @WebMvcTest class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private HelloController helloController;
@Test
public void greetingShouldReturnMessageFromService() throws Exception {
Mockito.when(service.greet()).thenReturn("Hello, Mock");
this.mockMvc.perform(get("/greeting")).andDo(print()).andExpect(status().isOk())
.andExpect(content().string(containsString("Hello, Mock")));
}
} ```
上面的程式碼我們用到了 Mockito, 可能你聽過周杰倫一首新歌叫 Mojito ,對,Mockito 的命名就是對 Mojito(一種傳統的古巴高球雞尾酒)的戲稱
簡單來說 Mockito
是一個 java 做單元測試的 Mock 框架:http://site.mockito.org/
解釋下我們上面這行程式碼 Mockito.when(service.greet()).thenReturn("Hello, Mock");
意為: 當呼叫 service 的 greet 方法的時候,返回值為 “Hello Mock”,其實沒真調那個方法,就是 Mock 了一下,直接給了個返回值。用英文說就是 :When the x method is called then return y
當然 Mockito 在假造上是很有實力的,它有豐富的 API 供你組合使用,有興趣可以看一看文件和原始碼註釋。
講到這兒,一定有同學會問,只測 Controller ,那我就用 Postman 就行 了,甚至 curl 都行,為啥要寫用例,我不寫用例。
哈哈,我相信很多後端同學都沒認認真真把用例寫完,尤其是 controller 這層的,不裝逼,我也是。那我們有必要討論一下 到底是用 Postman 還是用 MockMVC ?
首先說說 MockMVC 的好處:
- 可程式設計,這就給了你無限的自由空間,想怎麼折騰隨便你,你是上帝
- 除了寫的時候花點時間外,除錯的時候速度快,而且可配置,你要想只測 controller,就只啟動 controller 的上下文就行了
- 順便把測試用例寫了,測試同學省心了,給自動化測試提供了基礎
- 間接提高程式碼質量
其他的我不說,我就說最後一點。我注意到一個現象,很多開發同學拿測試同學當工具人,自己寫的程式碼自己不怎麼測試,直接交給測試讓他們提 BUG,然後改,BUG 多也不覺得害臊。開發是爽了,由於程式碼質量差,整個專案的進度都被拖慢了。你可能會說這是軟體質量管理的問題,是規則制定的有問題,如果出 BUG 扣錢就沒這事兒了。
我要說的是,在軟體開發這個領域,很多事情不是刻板的死規則,即便是制定了這樣的規則,也不一定有效。更多的時候是整個團隊的文化和風氣,領導者有責任將整個研發團隊的文化和風氣帶向正軌。什麼是正軌 ? 其實我們都知道! 我們都知道應該寫高質量的程式碼,bug 少的程式碼,設計合理的程式碼,不斷重構、不斷維護的程式碼,我們都知道要做好自己的事就會提高整個團隊的效率,我們都知道應該寫註釋、寫文件,我們都知道.....
我們都知道,但我們也知道專案時間緊,而且專門有人一遍遍強調 deadLine ,有人關心你的開發進度,關心功能實現了沒有,關心老闆有沒有意見,沒有人關心你累不累,關心你幾點下的班,關心規劃合不合理,關心程式碼質量高不高,關心與軟體真正有關係的一切。所以做一個真正的 軟體研發團隊的 Leader 不容易,遇到好 Leader 是你的福氣。
扯多了,我們回頭來看 Postman ,Postman 的好處好像也不用我多說了,確實,如果只是簡單的做 Controller 連通測試,用 Postman 一點兒問題沒有,也比寫程式快,但如果你的需求時有正好是 MockMVC 的優點可以覆蓋的地方,那麼就動動手,寫寫程式吧。
測試報告
想成一份漂亮的測試報告 ? 後端同學說了,整那花裡胡哨的有啥用呢,簡單一點兒不好嗎?
好,簡單點兒當然可以,但 UI 帶給我們的價值不就是一圖勝千言嘛,讓無論是前端、後端、測試同學都能一目瞭然,減輕大腦處理資訊的成本。
來,我們先上成果
怎麼樣,還挺好看的吧,我們用的是 Allure 來生成了一個 web 頁面,這個頁面還有一些簡單的互動,整體簡潔好看、易用。
下面我們說一下 Allure 怎麼和 JUnit 整合的
我們仍然使用 SpringBoot 以及 JUnit 5 ,先修改一下 pom.xml 檔案,新增依賴
```XML
```
然而我們新增在 build 中兩個 plugin
```XML
</plugin>
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>2.11.2</version>
<configuration>
<reportVersion>2.4.1</reportVersion>
</configuration>
</plugin>
```
我們用 brew 在本地安裝一下 Allure (我是 mac 就用這個裝了,如果你是其他環境參考後面說的文件)
```Bash brew install allure
```
接著我們調整專案中的測試用例,然後執行:
Bash
mvn clean test
接著找到你專案中 surefire-reports
的目錄位置
然後執行類似如下命令:
```Bash
注意路徑改成你自己專案的,這裡只是示例
allure serve /home/path/to/project/target/surefire-reports/
```
顯示如下資訊會自動跳轉到瀏覽器,開啟測試報告頁面。
是不是很簡單?
有關 Allure 安裝和使用說明請參考: http://docs.qameta.io/allure-report
有關 JUnit5 就聊到這兒,日常一般的開發是夠用了。更多的細節和功能,紹假設 、 斷言等,請看官方文件 ,當然備不注它也有錯的時候。
TestNG
TestNG is a testing framework inspired from JUnit and NUnit but introducing some new functionalities that make it more powerful and easier to use, such as:
- Annotations.
- Run your tests in arbitrarily big thread pools with various policies available (all methods in their own thread, one thread per test class, etc...).
- Test that your code is multithread safe.
- Flexible test configuration.
- Support for data-driven testing (with @DataProvider).
- Support for parameters.
- Powerful execution model (no more TestSuite).
- Supported by a variety of tools and plug-ins (Eclipse, IDEA, Maven, etc...).
- Embeds BeanShell for further flexibility.
- Default JDK functions for runtime and logging (no dependencies).
- Dependent methods for application server testing.
上面是 TestNG 的官方介紹,看起來比 JUnit 功能還強大。有了前面 Junit 作為引子, 你再看 TestNG,就好理解的多,因為概念上都差不多,只是功能和細節的不同而已。在這裡我們不會展開講 TestNG 了,但是會討論一下選型的問題。
如果在 JUnit 5 沒出來之前,比如 JUnit4 和 3 的時代,我會毫不猶豫地選擇 TestNG,為什麼?功能強大,好用啊。但是現在 JUnit5 來了,而且推廣的勢頭也很猛,重要的是從功能上也不輸 TestNG,那麼怎麼選呢?
個人覺得:
- 如果是後端開發,一般還是選 JUnit 5 寫單元測試方便簡單些,SpringBoot 也內建了 JUnit 開箱即用,從生態和社群上講即使有坑也好解決些
- 如果是搞自動化測試的同學,更多的可能還是用 TestNG 方便些,之前很多遺留專案都是用的 TestNG,另外它和自動化測試工具 selenium 的搭配也早已深入人心。從設計理念到 API,都更符合測試同學的思維。
參考
- http://testng.org/doc/
- http://spring.io/guides/gs/testing-web/
- http://cloud.tencent.com/developer/article/1779117
- http://blog.csdn.net/qq_39466683/article/details/121911310
- http://tonydeng.github.io/2017/10/10/junit-5-annotations/
- http://junit.org/junit5/docs/current/user-guide/
- http://www.liujiajia.me/2021/5/14/why-exclude-junit-vintage-engine-by-default
- 聊聊 Java 的單元測試
- 虛擬人直播-元宇宙離我們有多遠?
- B 樹葉子結點使用單向連結串列進行串連?錯!
- 聊聊ThreadLocal
- 有關 COW (CopyOnWrite) 的一切
- 我就想存個檔案,怎麼這麼麻煩 ?- k8s PV、PVC、StorageClass 的關係
- istio 原理簡介
- 冪等解決方案集合(二)訊息冪等
- 冪等解決方案集合(一)
- 百度 UidGenerator 原始碼解析
- 如何利用 VsCode evernote markdown 將自己的部落格自動同步成筆記
- 徹底理解 AQS 我是懂了,你呢?
- # ShardingSphere 分庫分表--第(1)篇
- ShardingSphere 實現資料加密(脫敏)第一篇
- SpringBoot ShardingSphere-JDBC 實現讀寫分離
- 我對MySQL鎖、事務、MVCC 的一些認識
- 分散式事務:從理論到實踐(一)建議收藏
- spring cloud 二代架構依賴元件 docker全配置放送(一)
- API 閘道器選型及包含 BFF 的架構設計
- 如何使用skywalking 進行全鏈路監控