---
# System prepended metadata

title: 六角鼠年鐵人賽 Week 14 - Spring Boot - JUnit 5 單元測試
tags: [Spring Boot, w3HexSchool]

---

# 六角鼠年鐵人賽 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`