JUnit 5.6 新功能讓測試變簡單
定義測試順序及平行測試的能力讓這成為一個重要的版本
Translated from “JUnit 5.6 Makes Testing Easy with New Features” By Mert Çalişkan, Java Magazine May 4, 2020. Copyright Oracle Corporation.
廣泛使用的 Java 測試框架 JUnit,2017 年有個重要的里程碑,發行 JUnit 5,在 4.x 演進了近十年後,提供一個完全重寫的框架。在 5.0.0 大版本後,JUnit 團隊設定一個快速的開發步調,每四到五個月發布一個小版本,最新的版本是 5.6.0,於 2020 年 1 月 20 日發布,並在 3 月 22 日更新至 5.6.1。
我將重訪這框架,用程式範例展示它帶上檯面的新功能,我會用「從 5.x」的標註描述相關的功能,強調那些功能是在哪個版本被加入到框架中。
首先,快速的術語定義,JUnit 5 分成三個模組:
- JUnit Platform 是在 JVM 中啟動測試框架的基礎,它由許多 IDE 與建置工具支援。
- JUnit Jupiter 是 JUnit 5 測試最新的程式模型及引擎。
- 最後 JUnit Vintage 是 JUnit 3 及 JUnit 4 測試案例的引擎。
(編按:您手編若有 JUnit 4 的測試案例,請參越考 Brian McGlauflin’s 的《從 JUnit 4 轉移到 JUnit 5:重要的差異與好處》[譯註:有空再翻譯])
如果您使用 Maven,JUnit 5.6 可以輕易地被加入如 Listing 1 所示。
若是使用 Gradle,可以如 Listing 2 加入 JUnit。
我們接著來看新功能吧!
定義測試的執行順序
您可以用 @Order
定義一個類別中的測試函式之間的執行順序,照字母排列或隨機 (自 5.4 版)。在類別層級使用 @TestMethodOrder
可已用您希望的順序執行測試 (自 5.4 版)。
Listing 3 中的測試函式將以字母順序執行。執行順序會是 testA() -> testC() -> testZ()
。
亦可以隨機順序,使用 @TestMethodOrder(MethodOrderer.Random.class)
,測試函式會以隨機的方式選取來執行。當測試之間不該有相依,而您希望確保這件事時,這就很有意義,如此,接下來的階段,測試之間不會有不必要的關聯。
如果您想明確指定測式的順序,如 Listing 4 所示,您可以使用 @Order
。在 Listing 4 中,最小值最優先,因此 testZ()
先執行,然後 testC()
,沒有被標註 @Order
的函式,會有預設值 Integer.MAX_VALUE / 2
(自 5.6 版)。
也可以自行定義排序機制並提供給 @TestMethodOrder
,Listing 5 中的實作以測試函式名稱的長度排序,短的會比長的先執行。
如 Listing 6 所示,將 CustomOrder
帶給 @TestMethodOrder
。然後,執行順序會是 short_mthd() -> a_very_long_test_method()
。
定義描述性逾時
如果花費超過指定的時間門檻,JUunit 可將測試視為失敗,這在診斷任何長時間執行的測試時十分有用,從 JUnit 5.5 開始,可以像 Listing 7 那樣,用 @Timeout
的標註 [譯註:本文中所有的 annotation 都譯作標註,而非註釋] 定義逾時。
在 checkJobDoesNotExceedLimit()
測試函式上,設為 1 秒逾時,這是預設的時間單位,很明顯這測試會失敗因為至少要執行 1,500 毫秒,可以如 Listing 8,指定不同的時間單位。
也可以用系統屬性來定義逾時,Listing 9 定義每個測試函式執行不可超過 100 毫秒。
如果有使用標註定義逾時,那會覆寫系統屬性定義的數值,這裡列出所有相關的系統屬性與解釋
junit.jupiter.execution.timeout.default
:對所有可執行與生命週期 [譯註:@BeforeEach
、@AfterEach
等]的函式定義逾時。junit.jupiter.execution.timeout.testable.method.default
:對所有可測試的函式定義逾時。junit.jupiter.execution.timeout.test.method.default
:對有所有有標註@Test
的函式定義逾時。junit.jupiter.execution.timeout.testtemplate.method.default
:對有所有有標註@TestTemplate
的函式定義逾時。junit.jupiter.execution.timeout.testfactory.method.default
:對有所有有標註@TestFactory
的函式定義逾時。junit.jupiter.execution.timeout.lifecycle.method.default
:對所有生命週期的函式定義逾時。junit.jupiter.execution.timeout.beforeall.method.default
:對有所有有標註@BeforeAll
的函式定義逾時。junit.jupiter.execution.timeout.beforeeach.method.default
:對有所有有標註@BeforeEach
的函式定義逾時。junit.jupiter.execution.timeout.aftereach.method.default
:對有所有有標註@AfterEach
的函式定義逾時。junit.jupiter.execution.timeout.afterall.method.default
:對有所有有標註@AfterAll
的函式定義逾時。
@Timeout
標註可以加在類別層級,提供該類別所有測試函式的預設值,同樣,@Timeout
也可用在用 @BeforeAll
、 @BeforeEach
、 @AfterEach
或 @AfterAll
標註的函式上。目前 @Timeout
在 JUnit 5.6 是標為實驗性,所以在接下來的版本可能會修改。
條件式測試執行
從 JUnit 5.1 開始,可以用標註內的定義以程式化的方式啟用或禁止測試的執行,這節,我將介紹不同類型的標註及其用法。
JRE 條件。根據 JRE 的版本啟用或禁止測試執行是可行的,@EnabledOnJre
(自 5.1 版) 只在特定的 JRE 版本才會啟用該測試的執行,因此,Listing 10 中的測試函是只會在 Java 8 上執行。
正好相反,@DisabledOnJre
(自 5.1 版) 禁止測試在特定版本執行,從 JUnit 5.6 開始,可以指定 JRE 版本的範圍,從 8 到 15。@EnabledForJreRange
和 @DisabledForJreRange
兩個標註 (都自 5.6 版) 定義執行的條件,啟用或禁止特定的 JRE 版本範圍,Listing 11 展示測試在 JRE 版本範圍才啟用的例子。
作業系統條件。也可根據作業系統版本將測試啟用或禁止,@EnabledOnOs
標註 (自 5.1 版),如 Listing 12 所示,只在 macOS 啟用該測試的執行。與之相反,@DisabledOnOS
標註 (自 5.1 版) 禁止測試在指定的作業系統上執行,可指定的作業系統有 AIX
、LINUX
、MAC
、SOLARIS
、WINDOWS
及 OTHER
。
環境變數條件。測試的執行與否可根據環境變數的存在來決定,標註 (自 5.1 版) @EnabledIfEnvironmentVariable
啟用測試的執行如果有環境變數的值符合指定的正規表示法,與之相反,@DisabledIfEnvironmentVariable
標註 (自 5.1 版) 條件相同則是禁止測試的執行,Listing 13 示範如果環境變數 HOME
的值是 /Users/mcaliskan
或 /Users/mertcaliskan
才執行被標註的測試。
系統屬性條件。可根據系統屬性的存在與否決定測試是起用還是禁止,標註 (自 5.1 版) @EnabledIfSystemProperty
啟用測試的執行如果系統屬性的值符合指定的正規表示法,反之,相同條件 @DisabledIfSystemProperty
則是禁止測試的執行,Listing 14 展示如何根據是否存在 os.arch 系統屬性啟用標註的測試,這測試只會 64 位元版本的 JRE 上執行。
@EnabledIfEnvironmentVariable
、@DisabledIfEnvironmentVariable
、@EnabledIfSystemProperty
、@DisabledIfSystemProperty
在 JUnit 5.6 都支援重複標註,因此可在同個測試函式上定義多次。JUnit 5.6 已經將上個版本就宣告捨棄的 @EnabledIf
和 @DisabledIf
標註相關的程式碼移除,簡易使用上述類別中的標註。
程式化擴充的註冊
擴充的主要目的是提供控展測試類別或函式的行為,並在多個測試中重複利該邏輯,JUnit 5 透過導入擴充機制提供一種優雅的方式,透過擴充 API,可以在執行週期中的特定點暫停測試的執行,然後執行掛載的擴充邏輯。利用 @RegisterExtension
標註 (自 5.1 版) 可以程式的方式註冊擴充,Listing 15 是個紀錄執行時間的範例程式。
Listing 16 則是將記錄程式執行時間的擴充註冊到測試類別中。
從 JUnit 5.5 開始,註冊擴充有些限制,其一是 RegisterExtension
不能標註在私有的欄位,另一個是擴充的類別至少要實作一個 Extension APIs,在 5.4 板之前,沒有成功配置的擴充將被忽略。
平行執行測試
預設情況下,JUnit Jupiter 以單執行緒循序執行測試,但從 5.3 開始,平行執行測試是可行的。有個重要的警告,這是實驗中的功能。
首先,透過系統屬性將 junit.jupiter.execution.parallel.enabled
設為 true
,測試函式或所屬類別要標註 @Execution(ExecutionMode.CONCURRENT)
(自 5.3 版),Listing 17 展示一個簡單的例子,該端點將同時進行五次測試,標註 @RepeatedTest
的實作是用 @TestTemplate
標註組合而成。
系統屬性 junit.jupiter.execution.parallel.mode.default (A)
及junit.jupiter.execution.parallel.mode.classes.default (B)
配置平行執行,二者都可以設為 concurrent
或 same_thread
來啟用平行執行。Figure 1 示範測試函式及類別執行的差異,第一欄是配置 A 與 B 配置的值。更多測試平行執行的細節可參閱文件。
結論
JUnit 5 團隊以四到五個月的節奏在 5.x 的分支上進行大量的工作,很高興能在重新設計的 JUnit 上擁有新的功能。
我已經介紹 5.0 到 5.6 版本之間釋出的主要功能,值得一提的是目前新的 JUnit 包含 OSGi metadata [譯註:目前幾個常見的翻譯我都不是很喜歡,所以保留原字],您可以您喜歡的 OSGi 容器中輕鬆地使用它,另一個是從 5.6 開始,JUnit 亦發布 Gradle 模組的 metadata ,Gradle 的使用者可以以細顆粒度的方式解析各式差異的相依關係。
注意,部分 JUnit 5 的 API 仍可能改變,開發團隊為公開的型別用 @API
標註 Experimental
、Maintained
及Stable
等狀態。
了解更多
後記
還記得 JUnit 5 剛出時,Java Magazine 用一期的專刊介紹 JUnit 5,可見 JUnit 對 Java 的生態圈來說有多重要,當時我也把那一期的每篇文章都翻譯了:《Part 1: Unit 5 初探》、《Part 2: 使用 JUnit 5》、《深入探討 JUnit 5 的擴充模型》和《訪談 Kent Beck》,其中最喜歡的是最後一篇,從中學到很多設計的哲學,沒想到這麼快,已經三年 (2017 年至今) 過去了,也許該找個時間把 logdown 的文章搬過來。
Java Magazine 預告接下來要慶祝 Java 25 週年,雖然我已經很少在工作的環境中用到 Java,但還是持續在關注 Java,它仍是我喜歡的語言之一,雖然最近 Java 的版本發行步調也變快很多,但始終覺得 Java 開發團隊還是以相對比較 heavy-way 的方式在開發 Java 新語言特性,對於較輕量的軟體開發需求,就會覺得 Java 的生產力不怎麼高,有趣的是,六年前我曾在《希望 Java 未來能新增的特性》提到的幾個特性,幾乎都可以在 Kotlin 中找到,而不是 Java。