# 六角鼠年鐵人賽 Week 14 - Spring Boot - JUnit 5 單元測試
==大家好,我是 "為了拿到金角獎盃而努力著" 的文毅青年 - Kai==
## 名句 Kobe Bryant
:::info
Everything negative — pressure, challenges — is all an opportunity for me to rise.
:::
## 主題
今天來跟大家分享的是做單元測試專用的套件 JUnit (版本5)。
這應該是一個在任何網路Java貼文中,只要涉及程式教學的專案幾乎都會引用的套件。
工程師都會同意的就是程式上線前必須要經過測試,通過專業測試員的檢測才能知道程式到底有哪些操作會引發錯誤?功能在任何情境下是否都能正常運作?資訊安全?系統負載能力等等。
這件事情在系統尚未發展成巨型架構、搭載許多功能時通常不會有任何問題。但在系統越趨成熟或為了分流產出了許多客製化的功能後,測試這件事情就會越來越棘手。
舉例:
系統一共有 30個功能,測試員接收 release 後,需要花費五天測試180個試驗並產出結果,平均一個功能會用上六種情境。
半年後,系統擴充到了60個功能,我們都會期望的是測試員同樣在每個功能上用上六種情境,並產出360個試驗結果出來。這時測試員可能會需要十天。
但現實往往沒有這麼美好,如果說只能給同樣5天呢? 測試員就得斟酌降低每一個功能使用的情境;如果說系統多出來的30功能中,有10個在A案,另外20個在B案,那測試員要測試的便是40+50=90個功能。
這都還只是很小的數字,巨型系統諸如銀行、電商等,可能達到上百上千的功能,人工偵測、除錯有其極限且所費不貲 (時間就是金錢)。
因此測試這件事情本身也逐漸需要被程序化,畢竟測試本身是一件變化性質少且重複性質高的事情,單元測試、功能測試、系統測試等都可以被自動化處理。
## 介紹 JUnit 5
JUnit 起於 1997 年,由 Kent Beck 和 Erich Gamma 兩位歷史級的工程師,在旅程的飛機上合作構思出的架構,專門給 Java 做程式測試用的,當時並沒有一套成熟、完整的體系可以增進 Java 工程師在開發上的難處,因此在 JUnit 橫空出世後,大幅的減低了工程師的負擔,而測試流程的改變也增進了系統的品質,間接降低了企業費用的支出。
目前在 Java 來說,**JUnit** 和 **TestNG** 分別為兩大測試套件,他們都可以幫助工程師編寫可重複運行且架構工整的單元測試程式。而 JUnit 本身發展得比較久,從最初 1997 年開發的版本到 2019 年的 JUnit 5 也有20年的發展歷史,名稱上來說也比較好記,因此成為大多數要踏入學習單元測試的首選。
> 基本上大多數的功能兩者都在伯仲之間,最大的不同只是 **TestNG** 可以做 Dependent Tests,而 JUnit 5 並未支援此項目
目前來說 JUnit 已經到了第五個版本,且 JDK 的需求也提升到了 1.8 版本,可能會有部分的使用者不適應變化的部分,畢竟 Java 7 -> 8 包含了許多突破性的改變,詳細不在這裡說明,大家可以自行找些比較的文章了解^^
## JUnit 5 的三大模組
JUnit 5 由 **JUnit Platform**、**JUnit Jupiter**、**JUnit Vintage**
- Platform
- 是 JUnit 用於 JVM上啟動的服務,用於支持測試時候的程式
- Jupiter
- 是 JUnit 5 新的 Compile 和 Extends 模組,主要用於讀寫測試程式語法
- Vintage:
- 用於兼容 JUnit 4 和 JUnit 3 版本的模組
---
觀察下面的架構圖會更清楚其模組相依的關係
![](https://i.imgur.com/LkKZvIe.png)
## Why JUnit 5 ?
比起 JUnit 4 來說,JUnit 5 要求最低的 JDK 版本為 1.8 版,意味著在5版本是可以支持 Lambda 表達式與 Stream API。
> 這兩個功能就是 1.7 與 1.8 最大的分歧。相較於 Java 11 -> 12 還有著更多更大的影響。但這裡不說這個~
且 Junit 5 多了模組測試,可以讓測試程式解析模組解耦與依賴的問題,其他如支持動態測試、重複測試、參數化測試等。
上述都是讓 Kai 這邊選擇使用5版本來介紹而不是4或3版本的原因,畢竟這類不會動用到基本盤的套件,能用新版本的功能就選用新版本的,但需要強調使用的依舊是新版本中的穩定版本,產品內的絕對不要使用任何 beta 版的套件,這觀念套用在任何工作都適用,請絕對不要把還在嘗試的東西放入正式產品中。
## Gradle 設定
首先需要把 JUnit 5 的套件給引用,Kai 這邊使用的是 Gradle,因此只需要在 dependencies 中加入下面的套件名稱即可,因為只有在測試的時候使用,因此只需要設定 testImplementation 和 testRuntimeOnly。
```xml
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
}
```
## 常用的 Annotation 與其參數介紹
JUnit 5 與 4 和 3 一樣,都是透過 Annotation 的方式進行測試模組功能的呼叫與執行。
在個別的 Annotation 設定忠亦可加入部分功能提供的屬性參數,以增加對於執行測試的細節調整。
### @DisplayName(" [Input String] ")
說明:
測試類別或程式的名稱,會在執行前印出在 Console 上
撰寫位置:
宣告 Class 或 function 的程式碼前
### @BeforeAll
說明:
在該 Class 進行 @Test 的程式前,先行運行的程式,多用於定義物件或參數處理
撰寫位置:
宣告 function 的程式碼前
### @AfterAll
說明:
在該 Class 進行完所有 @Test 的程式後,才運行的程式,多用於釋放資源或整理結果訊息
撰寫位置:
宣告 function 的程式碼前
### @BeforeEach
說明:
在任何 @Test 進行前,先行運行的程式,多用於印出 Trace Log 或重整資源
撰寫位置:
宣告 function 的程式碼前
### @AfterEach
說明:
在任何 @Test 進行後,才運行的程式,多用於印出結果、Trace Log 或釋放資源
撰寫位置:
宣告 function 的程式碼前
### @Test
說明:
執行測試的程式,會依照設置程式碼的先後順序,優先執行排列較上面的 @Test 程式
撰寫位置:
宣告 function 的程式碼前
### @Disabled
說明:
標記該 Annotation 的類別或程式將不會被 JUnit 捕捉放入預備執行的程式列中,多為測試時候動態處理是否執行特定環節時候使用
撰寫位置:
宣告 Class 或 function 的程式碼前
### @Nested
說明:
用於內部類別的測試
撰寫位置:
宣告非 public 的 Class 的程式碼前
### @RepeatedTest( value = [Integer], name=" [Input String] ")
說明:
用於設定該測試需要重複的次數,另外可放入 name 的屬性做到輸出目前的次數與測試訊息
支援的參數如下:
- {displayName}: 會秀出 @DisplayName 的設定值
- {currentRepetition}: 會秀出目前執行的次數值
- {totalRepetitions}: 會秀出總共需要執行的次數值
範例:
name = "{displayName} 第{currentRepetition}次測試,總共需要執行{totalRepetitions}次"
撰寫位置:
宣告 function 的程式碼前
### @ParameterTest
須搭配資料來源方可建立參數化測試,可搭配的參數 Annotation 如下:
- @ValueSource:
支援 @NullSource、@EmptySource、@NullAndEmptySource
設立 Object 類型物件陣列值以作測試輸入值
**範例**:
```java=
@ValueSource(ints = {1,2,3})
@ValueSource(strings = {"1","2","3"})
@ValueSource(strings = {""," ",null,"\r\n"})
```
- @EnumSource:
支援使用 ENUM 類作測試輸入值,可在參數內輸入 ENUM 類,或是 function 內輸入後讓JUNIT 偵測目標 Class 來用,可輸入 name 和 mode 屬性去設定使用特定的 ENUM 類中的物件或排除使用特定物件
**範例**:
```java=
// 一般寫法
@EnumSource(Test.class)
public void testWithEnumSource(TemporalUnit unit)
// 讓 JUnit 自動偵測寫法
@EnumSource()
public void testWithEnumSourceAutoDetection(Test unit)
// 限定使用物件
@EnumSource(names = {"TestObject1","TestObject3"})
public void testWithEnumSourceAutoDetection(Test unit)
// 排除使用物件
@EnumSource(mode = EXCLUDE, names = {"TestObject2","TestObject4")
public void testWithEnumSourceAutoDetection(Test unit)
// 支援 REGEX 寫法
@EnumSource(mode = MATCH_ALL, names = {"^.*TestObject$")
public void testWithEnumSourceAutoDetection(Test unit)
```
- @MethodSource:
支援使用類別方法作為測試輸入值,類別方法必須回傳 Stream 物件且其包含參數必須為繼承 Ojbect 類的物件。
**範例**:
```java=
// 一般寫法
@MethodSource("stringProvider")
public void testWithExplicitLocalMethodSource(String argument)
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
// 讓 JUnit 自動偵測寫法
@MethodSource()
public void testWithExplicitLocalMethodSource(String argument)
static Stream<String> testWithExplicitLocalMethodSource() {
return Stream.of("apple", "banana");
}
```
- @CsvSource
支援使用 CSV 格式輸入值作測試值,以逗號分隔,一列有多少個參數,則 function 便需要 input 多少個參數,可使用同一參數類型 ... 的方式處理。
另外 @CsvSource 預設以逗號切割一列的值,若字符串中間有逗號不想因此被切割,可以用單引號將其框住,避免完整物件值被切分開。
支援屬性如下
- value: 預設屬性,作測試輸入值的接收參數
- delimiterString: 可調整切割一列物件值的字串符號
- nullValues: 設定特定字串符,當物件值符合時會自動變更為 Null
**範例**:
```java=
// 一般寫法
@CsvSource({"apple,1","banana,2","'lemon, lime', 0xF1"})
public void testWithCsvSource(String fruit, int rank) {
// 當物件值為 NIL 時,將其設置為 Null
@CsvSource(value = { "apple, banana, NIL" }, nullValues = "NIL")
```
- @CsvFileSource
類似於 @CsvSource,但測試值為外部文件輸入,且不想被分割的物件值部分,@CsvSource 是用單引號,但 @CsvFileSource 則使用雙引號
支援屬性如下:
- resources: 資料來源
- numLinesToSkip: 略過行數,通常設為 1,因為多數 CSV 檔案第一行為資料欄位名稱
**範例**:
```java=
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
```
## 結語
:::danger
簡單介紹完了 JUnit 5,最重要的還是開發者撰寫出適合自己公司系統業務需求的測試程式,每一次更新就會增加一段給新功能的測試,同時因應面對的問題,增加不同例外狀況模擬,確保系統可以永遠保持在一個穩定的狀況中成長。
下一篇將介紹會合併一起使用於單元測試的框架 Mockito~
[六角鼠年鐵人賽 Week 15 - Spring Boot - Mockito 模擬測試框架](/g3YGfarYSA6iu8IBfyq1Gg)
:::
首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA)
###### tags: `Spring Boot`,`w3HexSchool`