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