此文爲對JUnit5官網用戶指南的學習,版本爲5.4.0版本,對部分內容進行翻譯、截取和自我理解,官網手冊地址爲https://junit.org/junit5/docs/current/user-guide/。 (中文的翻譯是5.3版本的)html
JUnit5最低須要JDK8來支持。java
與以前的版本不一樣,JUnit5由3個模塊組成:git
一、JUnit Platform,用於在JVM上啓動測試框架,並經過命令行定義TestEngine API。能夠至關於JUnit 4 中的Runner ,要用他作測試引擎。簡單地說這個有關的包是用來調用測試用例的,IDE正式由於加載了與這個有關的插件,因此idea裏邊才能夠右鍵選擇執行測試方法。github
二、JUnit Jupiter是用於在JUnit 5中編寫測試和擴展的新編程模型和擴展模型的組合。提供了一堆測試要用的註解和類。web
三、Junit Vintage,用於在JUnit5平臺上運行JUnit3和4測試用例。正則表達式
Group ID: org.junit.platformspring
Version: 1.2.0編程
Artifact IDs:api
junit-platform-commons數組
JUnit 內部通用類庫/實用工具,它們僅用於JUnit框架自己,不支持任何外部使用,外部使用風險自負。
junit-platform-console
支持從控制檯中發現和執行JUnit Platform上的測試。詳情請參閱 控制檯啓動器。
junit-platform-console-standalone
一個包含了Maven倉庫中的 junit-platform-console-standalone 目錄下全部依賴項的可執行JAR包。詳情請參閱 控制檯啓動器。
junit-platform-engine
測試引擎的公共API。詳情請參閱 插入你本身的測試引擎。
junit-platform-gradle-plugin
支持使用 Gralde 來發現和執行JUnit Platform上的測試。
junit-platform-launcher
配置和加載測試計劃的公共API – 典型的使用場景是IDE和構建工具。詳情請參閱 JUnit Platform啓動器API。
junit-platform-runner
在一個JUnit 4環境中的JUnit Platform上執行測試和測試套件的運行器。詳情請參閱 使用JUnit 4運行JUnit Platform。
junit-platform-suite-api
在JUnit Platform上配置測試套件的註解。被 JUnit Platform運行器 所支持,也有可能被第三方的TestEngine實現所支持。
junit-platform-surefire-provider
支持使用 Maven Surefire 來發現和執行JUnit Platform上的測試。
Group ID: org.junit.jupiter
Version: 5.3.0
Artifact IDs:
junit-jupiter-api
junit-jupiter-engine
JUnit Jupiter測試引擎的實現,僅僅在運行時須要。
junit-jupiter-params
支持JUnit Jupiter中的 參數化測試。
junit-jupiter-migration-support
支持從JUnit 4遷移到JUnit Jupiter,僅在使用了JUnit 4規則的測試中才須要。
Group ID: org.junit.vintage
Version: 5.3.0
Artifact ID:
junit-vintage-engine
JUnit Vintage測試引擎實現,容許在新的JUnit Platform上運行低版本的JUnit測試,即那些以JUnit 3或JUnit 4風格編寫的測試。
package TestJunit5; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; public class _1_test { @Test void testIntAddition() { assertEquals(2, 1 + 1); } } |
除非另有說明,不然全部核心註釋都位於junit-jupiter-api模塊的org.junit.jupiter.api包中。
註解 |
描述 |
@Test |
表示該方法是一個測試方法。與JUnit 4的@Test註解不一樣的是,它沒有聲明任何屬性,由於JUnit Jupiter中的測試擴展是基於它們本身的專用註解來完成的。這樣的方法會被繼承,除非它們被覆蓋。 |
@ParameterizedTest |
表示該方法是一個 參數化測試。這樣的方法會被繼承,除非它們被覆蓋。 |
@RepeatedTest |
表示該方法是一個 重複測試 的測試模板。這樣的方法會被繼承,除非它們被覆蓋。 |
@TestFactory |
表示該方法是一個 動態測試 的測試工廠。這樣的方法會被繼承,除非它們被覆蓋。 |
@TestInstance |
用於配置所標註的測試類的 測試實例生命週期。這些註解會被繼承。 |
@TestTemplate |
表示該方法是一個 測試模板,它會依據註冊的 提供者 所返回的調用上下文的數量被屢次調用。 這樣的方法會被繼承,除非它們被覆蓋。 |
@TestMethodOrder |
用於配置帶註釋的測試類的測試方法執行順序;相似於JUnit 4的@FixMethodOrder。註解能夠被繼承。 |
@DisplayName |
爲測試類或測試方法聲明一個自定義的顯示名稱。該註解不能被繼承。 |
@DisplayNameGeneration |
聲明測試類的自定義顯示名稱生成器。註解會被繼承。 |
@BeforeEach |
表示使用了該註解的方法應該在當前類中每個使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory註解的方法以前 執行;相似於JUnit 4的 @Before。這樣的方法會被繼承,除非它們被覆蓋。 |
@AfterEach |
表示使用了該註解的方法應該在當前類中每個使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory註解的方法以後 執行;相似於JUnit 4的 @After。這樣的方法會被繼承,除非它們被覆蓋。 |
@BeforeAll |
表示使用了該註解的方法應該在當前類中全部使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory註解的方法以前 執行;相似於JUnit 4的 @BeforeClass。這樣的方法會被繼承(除非它們被隱藏 或覆蓋),而且它必須是 static方法(除非"per-class" 測試實例生命週期 被使用)。 |
@AfterAll |
表示使用了該註解的方法應該在當前類中全部使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory註解的方法以後執行;相似於JUnit 4的 @AfterClass。這樣的方法會被繼承(除非它們被隱藏 或覆蓋),而且它必須是 static方法(除非"per-class" 測試實例生命週期 被使用)。 |
@Nested |
表示使用了該註解的類是一個內嵌、非靜態的測試類。@BeforeAll和@AfterAll方法不能直接在@Nested測試類中使用,(除非"per-class" 測試實例生命週期 被使用)。該註解不能被繼承。 |
@Tag |
用於聲明過濾測試的tags,該註解能夠用在方法或類上;相似於TesgNG的測試組或JUnit 4的分類。該註解能被繼承,但僅限於類級別,而非方法級別。 |
@Disable |
用於禁用一個測試類或測試方法;相似於JUnit 4的@Ignore。該註解不能被繼承。 |
@ExtendWith |
用於註冊自定義 擴展。該註解不能被繼承。 |
@RegisterExtension |
用於經過屬性以編程方式註冊擴展。這些屬性是繼承的,除非它們被(子類)屏蔽。 |
@TempDir |
用於經過生命週期方法或測試方法中的字段注入或參數注入來提供臨時目錄。位於org.junit.jupiter.api.io包中。 |
還有一些註解在實驗中。具體能夠看Experimental APIs 表。
測試類和測試方法能夠不爲public,但必須不爲private。
這裏先記得@BeforeAll和@AfterAll修飾的是靜態方法
這裏@DisplayName註解用來標註名稱,方便查看。
@DisplayNameGeneration,是一個用來生成DisplayName的註解,配合DisplayNameGenerator類使用。@DisplayName註解的優先級更高
class DisplayNameGeneratorDemo {
@Nested @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class A_year_is_not_supported { @Test void if_it_is_zero() { } @DisplayName("A negative value for year is not supported by the leap year computation.") @ParameterizedTest(name = "For example, year {0} is not supported.") @ValueSource(ints = {1, -4}) void if_it_is_negative(int year) { } } } |
生成結果以下,能夠看到使用這個註解以後,測試以後的名稱下劃線消失了,這是由於傳入了DisplayNameGenerator.ReplaceUnderscores.class,而後能夠看到的確是@DisplayName的優先級更高一些,下面的方法的名稱是@DisplayName傳入進去的。
而後若是想要自定義名稱生成的規則,能夠去繼承內部的ReplaceUnderscores 進行改進。
全部的JUnit Jupiter斷言都是 org.junit.jupiter.api.Assertions類中static方法。可使用Lambda表達式。
而後若是斷言不能知足要求,能夠導入第三方的斷言庫。
假設與斷言的區別:假設失敗則中止測試,斷言失敗則拋出錯誤。假設能夠配合斷言使用
@Test void testInAllEnvironments() { assumingThat("CI".equals(System.getenv("ENV")), () -> { // 假設成立纔去執行斷言 assertEquals(2, 2); }); // perform these assertions in all environments assertEquals(42, 42); } |
@Disabled能夠禁用測試,再或者經過自定義的 ExecutionCondition 來禁用 整個測試類或單個測試方法。
JUnit Jupiter中的 ExecutionCondition 擴展API容許開發人員以編程的方式基於某些條件啓用或禁用容器或測試。這種狀況的最簡單示例是內置的 DisabledCondition,它支持 @Disabled 註解(請參閱 禁用測試)。除了@Disabled以外,JUnit Jupiter還支持 org.junit.jupiter.api.condition包中的其餘幾個基於註解的條件,容許開發人員以 聲明的方式啓用或禁用容器和測試。
能夠經過 @EnabledOnOs 和 @DisabledOnOs 註釋在特定操做系統上啓用或禁用容器或測試。
能夠經過 @EnabledOnJre 和 @DisabledOnJre 註解在特定版本的Java運行時環境(JRE)上啓用或禁用容器或測試。
能夠經過 @EnabledIfSystemProperty 和 @DisabledIfSystemProperty 註解根據指定的JVM系統屬性的值啓用或禁用容器或測試。經過matches屬性提供的值將被解釋爲正則表達式。
根據對經過 @EnabledIf 或 [@DisabledIf](https://junit.org/junit5/docs/5.3.0/api/org/junit/jupiter/api/condition/DisabledIf.html) 註解配置的腳本的評估,JUnit Jupiter提供了 啓用或禁用 容器或測試的功能。腳本能夠用JavaScript,Groovy或任何其餘支持Java腳本API的腳本語言編寫,由JSR 223定義。
腳本綁定
如下名稱綁定到每一個腳本上下文,所以在腳本中使用。訪問器 經過簡單的String get(String name)方法提供對相似Map結構的訪問。
名稱 |
類型 |
描述 |
systemEnvironment |
accessor |
操做系統環境變量訪問器。 |
systemProperty |
accessor |
JVM 系統屬性訪問器。 |
JunitConfigurationParameter |
accessor |
配置參數訪問器。 |
JunitDisplayName |
String |
測試或容器的顯示名稱。 |
junitTags |
Set<String> |
全部分配給測試或容器的標記。 |
junitUniqueId |
String |
測試或容器的惟一ID。 |
能夠用@Tag標籤標記,而後後面能夠根據裏邊傳入的信息對測試進行發現和過濾。
標記的限制(trimmed指兩端空格被去掉)
標記不能爲null或空。
trimmed 的標記不能包含空格。
trimmed 的標記不能包含IOS字符。
trimmed 的標記不能包含一下保留字符。
,:逗號
(:左括號
):右括號
&:& 符號
|:豎線
!:感嘆號
能夠在類上標註@TestMethodOrder來聲明測試方法要有執行順序,裏邊能夠傳入三種類Alphanumeric、OrderAnnotation、Random,分別表明字母排序、數字排序、隨機。而後對方法加@Order註解裏邊傳入參數決定順序。
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(OrderAnnotation.class) class OrderedTestsDemo { @Test @Order(1) void nullValues() { // perform assertions against null values } @Test @Order(2) void emptyValues() { // perform assertions against empty values } @Test @Order(3) void validValues() { // perform assertions against valid values } } |
爲了隔離地執行單個測試方法,以及避免因爲不穩定的測試實例狀態引起非預期的反作用,JUnit會在執行每一個測試方法執行以前建立一個新的實例。這個」per-method」測試實例生命週期是JUnit Jupiter的默認行爲,這點相似於JUnit之前的全部版本。
若是你但願JUnit Jupiter在同一個實例上執行全部的測試方法,在你的測試類上加上註解@TestInstance(Lifecycle.PER_CLASS)便可。啓用了該模式後,每個測試類只會建立一次實例。所以,若是你的測試方法依賴實例變量存儲的狀態,你可能須要在@BeforeEach或@AfterEach方法中重置狀態。
"per-class"模式相比於默認的"per-method"模式有一些額外的好處。具體來講,使用了"per-class"模式以後,你就能夠在非靜態方法和接口的default方法上聲明@BeforeAll和 @AfterAll。所以,"per-class"模式使得在@Nested測試類中使用@BeforeAll和@AfterAll註解成爲了可能。
若是測試類或測試接口上沒有使用@TestInstance註解,JUnit Jupiter 將使用默認 的生命週期模式。標準的默認 模式是PER_METHOD。然而,整個測試計劃執行的默認值 是能夠被更改的。要更改默認測試實例生命週期模式,只需將junit.jupiter.testinstance.lifecycle.default配置參數 設置爲定義在TestInstance.Lifecycle中的枚舉常量名稱便可,名稱忽略大小寫。它也做爲一個JVM系統屬性、做爲一個傳遞給Launcher的LauncherDiscoveryRequest中的配置參數、或經過JUnit Platform配置文件來提供(詳細信息請參閱 配置參數)。
例如,要將默認測試實例生命週期模式設置爲Lifecycle.PER_CLASS,你可使用如下系統屬性啓動JVM。
-Djunit.jupiter.testinstance.lifecycle.default=per_class
可是請注意,經過JUnit Platform配置文件來設置默認的測試實例生命週期模式是一個更強大的解決方案,由於配置文件能夠與項目一塊兒被提交到版本控制系統中,所以可用於IDE和構建軟件。
要經過JUnit Platform配置文件將默認測試實例生命週期模式設置爲Lifecycle.PER_CLASS,你須要在類路徑的根目錄(例如,src/test/resources)中建立一個名爲junit-platform.properties的文件,並寫入如下內容。
junit.jupiter.testinstance.lifecycle.default = per_class
對應@Nested註解。沒有這個註解的話他就不測試內部類了。
@Nested測試類必須是非靜態嵌套類(即內部類),而且能夠有任意多層的嵌套。這些內部類被認爲是測試類家族的正式成員,但有一個例外:@BeforeAll和@AfterAll方法默認 不會工做。緣由是Java不容許內部類中存在static成員。不過這種限制可使用@TestInstance(Lifecycle.PER_CLASS)標註@Nested測試類來繞開。
以前版本不行,如今能夠了。
ParameterResolver 爲測試擴展定義了API,它能夠在運行時動態解析參數。若是一個測試的構造函數方法接收一個參數,這個參數就必須在運行時被一個已註冊的ParameterResolver解析。
目前有三種被自動註冊的內置解析器。
①TestInfoParameterResolver:若是一個方法參數的類型是 TestInfo,TestInfoParameterResolver將根據當前的測試提供一個TestInfo的實例用於填充參數的值。而後,TestInfo就能夠被用來檢索關於當前測試的信息,例如:顯示名稱、測試類、測試方法或相關的Tag。顯示名稱要麼是一個相似於測試類或測試方法的技術名稱,要麼是一個經過@DisplayName配置的自定義名稱。
簡單的說就是參數是TestInfo類型,那麼就能夠在函數中調用,這個類包含不少測試的信息。
import org.junit.jupiter.api.TestInfo; @DisplayName("TestInfo Demo") class TestInfoDemo { TestInfoDemo(TestInfo testInfo) { assertEquals("TestInfo Demo", testInfo.getDisplayName()); } @BeforeEach void init(TestInfo testInfo) { String displayName = testInfo.getDisplayName(); assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()")); } @Test @DisplayName("TEST 1") @Tag("my-tag") void test1(TestInfo testInfo) { assertEquals("TEST 1", testInfo.getDisplayName()); assertTrue(testInfo.getTags().contains("my-tag")); } @Test void test2() { } } |
②RepetitionInfoParameterResolver:若是一個位於@RepeatedTest、@BeforeEach或者@AfterEach方法的參數的類型是 RepetitionInfo,RepetitionInfoParameterResolver會提供一個RepetitionInfo實例。而後,RepetitionInfo就能夠被用來檢索對應@RepeatedTest方法的當前重複以及總重複次數等相關信息。可是請注意,RepetitionInfoParameterResolver不是在@RepeatedTest的上下文以外被註冊的。請參閱 重複測試示例。
③TestInfoParameterResolver:若是一個方法參數的類型是 TestReporter,TestReporterParameterResolver會提供一個TestReporter實例。而後,TestReporter就能夠被用來發布有關當前測試運行的其餘數據。這些數據能夠經過 TestExecutionListener 的reportingEntryPublished()方法來消費,所以能夠被IDE查看或包含在報告中。
在JUnit Jupiter中,你應該使用TestReporter來代替你在JUnit 4中打印信息到stdout或stderr的習慣。使用@RunWith(JUnitPlatform.class)會將報告的全部條目都輸出到stdout中。
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestReporter; class TestReporterDemo { @Test void reportSingleValue(TestReporter testReporter) { testReporter.publishEntry("a key", "a value"); } @Test void reportSeveralValues(TestReporter testReporter) { HashMap<String, String> values = new HashMap<>(); values.put("user name", "dk38"); values.put("award year", "1974"); testReporter.publishEntry(values);//會在控制檯輸出 } } |
其餘的參數解析器必須經過@ExtendWith註冊合適的 擴展 來明確地開啓。
能夠查看 RandomParametersExtension 獲取自定義 ParameterResolver 的示例。雖然並不打算大量使用它,但它演示了擴展模型和參數解決過程當中的簡單性和表現力。MyRandomParametersTest演示瞭如何將隨機值注入到@Test方法中。
@ExtendWith(RandomParametersExtension.class) class MyRandomParametersTest { @Test void injectsInteger(@Random int i, @Random int j) { assertNotEquals(i, j); } @Test void injectsDouble(@Random double d) { assertEquals(0.0, d, 1.0); } } |
對於真實的使用場景,請查看 MockitoExtension 和 SpringExtension 的源碼。
JUnit Jupiter容許將@Test、@RepeatedTest、@ParameterizedTest、@TestFactory、TestTemplate、@BeforeEach和@AfterEach註解聲明在接口的default方法上。若是 測試接口或測試類使用了@TestInstance(Lifecycle.PER_CLASS)註解(請參閱 測試實例生命週期),則能夠在測試接口中的static方法或接口的default方法上聲明@BeforeAll和@AfterAll。
能夠在測試接口上聲明@ExtendWith和@Tag,以便實現了該接口的類自動繼承它的tags和擴展。
@RepeatedTest中填入次數能夠重複測試。
除了指定重複次數以外,咱們還能夠經過@RepeatedTest註解的name屬性爲每次重複配置自定義的顯示名稱。此外,顯示名稱能夠是由靜態文本和動態佔位符的組合而組成的模式。目前支持如下佔位符。
{displayName}: @RepeatedTest方法的顯示名稱。
{currentRepetition}: 當前的重複次數。
{totalRepetitions}: 總的重複次數。
一個特定重複的默認顯示名稱基於如下模式生成:"repetition {currentRepetition} of {totalRepetitions}"。所以,以前的repeatTest()例子的單個重複的顯示名稱將是:repetition 1 of 10, repetition 2 of 10,等等。若是你但願每一個重複的名稱中包含@RepeatedTest方法的顯示名稱,你能夠自定義本身的模式或使用預約義的RepeatedTest.LONG_DISPLAY_NAME。後者等同於"{displayName} :: repetition {currentRepetition} of {totalRepetitions}",在這種模式下,repeatedTest()方法單次重複的顯示名稱長成這樣:repeatedTest() :: repetition 1 of 10, repeatedTest() :: repetition 2 of 10,等等。
爲了以編程方式獲取有關當前重複和總重複次數的信息,開發人員能夠選擇將一個RepetitionInfo的實例注入到@RepeatedTest,@BeforeEach或@AfterEach方法中。
@RepeatedTest(2) void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) { assertEquals(2, repetitionInfo.getTotalRepetitions()); }
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}") @DisplayName("Repeat!") void customDisplayName(TestInfo testInfo) { assertEquals(testInfo.getDisplayName(), "Repeat! 1/1"); }
@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME) @DisplayName("Details...") void customDisplayNameWithLongPattern(TestInfo testInfo) { assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1"); }
@RepeatedTest(value = 2, name = "Wiederholung {currentRepetition} von {totalRepetitions}") void repeatedTestInGerman() { // ... } |
測試結果以下圖所示。簡單地說就是能夠在註解的name屬性中傳入測試名與佔位符,這樣在測試輸出的時候更容易經過名稱確認是哪一個測試的哪一次。而後能夠用以前的方法傳參章節中的內容,經過在測試方法中傳入TestInfo或RepetitionInfo類來實現測試的方法體中獲取測試參數,如獲取總共測試多少次,得到當前是第幾回。
使用@ParameterizedTest註解,參數化測試使得測試能夠測試屢次使用不一樣的參數值。
爲了使用參數化測試,你必須添加junit-jupiter-params依賴。
@ParameterizedTest @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void palindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); } |
參數化測試方法一般會在參數源索引和方法參數索引之間採用一對一關聯(請參閱 @CsvSource 中的示例)以後直接從配置的源中消耗參數(請參閱 參數源)。可是,參數化測試方法也能夠選擇未來自源的參數聚合 爲傳遞給該方法的單個對象(請參閱 參數聚合)。其餘參數也能夠由ParameterResolver提供(例如,獲取TestInfo,TestReporter等的實例)。具體而言,參數化測試方法必須根據如下規則聲明形式參數。
首先必須聲明零個或多個索引參數。
接下來必須聲明零個或多個聚合器。
由ParameterResolver提供的零個或多個參數必須聲明爲最後一個。
1、@ValueSource是最簡單的來源之一。它容許你指定單個數組的文字值,而且只能用於爲每一個參數化的測試調用提供單個參數。
@ValueSource支持如下類型的字面值:
short
byte
int
long
float
double
char
java.lang.String
java.lang.Class
爲了測試一些極端狀況,null啊或者爲空之類的,還提供了一些額外的註解。
@NullSource註解用來給參數測試提供一個null元素,要求傳參的類型不能是基本類型(基本類型不能是null值)
@EmptySource爲java.lang.String, java.util.List, java.util.Set, java.util.Map, primitive arrays (e.g., int[], char[][], etc.), object arrays (e.g.,String[], Integer[][], etc.)類型提供了空參數。
@NullAndEmptySource 註解爲上邊兩個註解的合併。
@ParameterizedTest @NullSource @EmptySource @ValueSource(strings = { " ", " ", "\t", "\n" }) void nullEmptyAndBlankStrings(String text) { assertTrue(text == null || text.trim().isEmpty()); } |
能夠看到@VauleSource中只有4個參數,但卻有6個測試,另外兩個是經過註解添加的。
2、@EnumSource可以很方便地提供Enum常量。該註解提供了一個可選的names參數,你能夠用它來指定使用哪些常量。若是省略了,就意味着全部的常量將被使用,就像下面的例子所示。
@ParameterizedTest @EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" }) void testWithEnumSourceInclude(TimeUnit timeUnit) { assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit)); } |
@EnumSource註解還提供了一個可選的mode參數,它可以細粒度地控制哪些常量將會被傳遞到測試方法中。例如,你能夠從枚舉常量池中排除一些名稱或者指定正則表達式,以下面代碼所示。第一個exclude是排除names中的枚舉類型,第二個是匹配names中的枚舉類型。
@ParameterizedTest @EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = {"DAYS", "HOURS"}) void testWithEnumSourceExclude(TimeUnit timeUnit) { assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit)); assertTrue(timeUnit.name().length() > 5); } @ParameterizedTest @EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$") void testWithEnumSourceRegex(TimeUnit timeUnit) { String name = timeUnit.name(); assertTrue(name.startsWith("M") || name.startsWith("N")); assertTrue(name.endsWith("SECONDS")); } |
3、@MethodSource容許你引用測試類或外部類中的一個或多個工廠 方法。
除非使用@TestInstance(Lifecycle.PER_CLASS)註解標註測試類,不然測試類中的工廠方法必須是static的。 而外部類中的工廠方法必須始終是static的。 此外,此類工廠方法不能接受任何參數。
每一個工廠方法必須生成一個參數流,而且流中的每組參數將被做爲被@ParameterizedTest標註的方法的單獨調用的物理參數來提供。 通常來講,這會轉換爲Arguments的Stream(即,Stream<Arguments>); 可是,實際的具體返回類型能夠採用多種形式。 在此上下文中,」流?是JUnit能夠可靠地轉換爲Stream的任何內容,例如Stream,DoubleStream,LongStream,IntStream,Collection,Iterator,Iterable,對象數組或基元數組。 流中的」參數」能夠做爲參數的實例,對象數組(例如,Object[])提供,或者若是參數化測試方法接受單個參數,則提供單個值。
@ParameterizedTest @MethodSource("stringProvider") void testWithSimpleMethodSource(String argument) { assertNotNull(argument); }
static Stream<String> stringProvider() { return Stream.of("foo", "bar"); } |
若是你未經過@MethodSource明確提供工廠方法名稱,則JUnit Jupiter將按照約定去搜索與當前@ParameterizedTest方法名稱相同的工廠方法。
若是參數化測試方法聲明瞭多個參數,則須要返回Arguments實例或對象數組的集合,流或數組,以下所示(有關支持的返回類型的更多詳細信息,請參閱@MethodSource的JavaDoc)。 請注意,arguments(Object ...)是Arguments接口中定義的靜態工廠方法。
@ParameterizedTest @MethodSource("stringIntAndListProvider") void testWithMultiArgMethodSource(String str, int num, List<String> list) { assertEquals(3, str.length()); assertTrue(num >=1 && num <=2); assertEquals(2, list.size()); } static Stream<Arguments> stringIntAndListProvider() { return Stream.of( Arguments.of("foo", 1, Arrays.asList("a", "b")), Arguments.of("bar", 2, Arrays.asList("x", "y")) ); } |
此外,能夠引用別的類裏邊的靜態工廠方法。用#分隔類和方法
class ExternalMethodSourceDemo {
@ParameterizedTest @MethodSource("example.StringsProviders#tinyStrings") void testWithExternalMethodSource(String tinyString) { // test with tiny string } }
class StringsProviders {
static Stream<String> tinyStrings() { return Stream.of(".", "oo", "OOO"); } } |
4、@CsvSource容許你將參數列表定義爲以逗號分隔的值(即String類型的值)。
@CsvSource使用單引號'做爲引用字符。請參考上述示例和下表中的'baz,qux'值。一個空的引用值''表示一個空的String;而一個徹底空的值被當成一個null引用。若是null引用的目標類型是基本類型,則會拋出一個ArgumentConversionException。
示例輸入 |
生成的參數列表 |
@CsvSource({ "foo, bar" }) |
"foo", "bar" |
@CsvSource({ "foo, 'baz, qux'" }) |
"foo", "baz, qux" |
@CsvSource({ "foo, ''" }) |
"foo", "" |
@CsvSource({ "foo, " }) |
"foo", null |
@ParameterizedTest @CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" }) void testWithCsvSource(String first, int second) { assertNotNull(first); assertNotEquals(0, second); } |
5、@CsvFileSource容許你使用類路徑中的CSV文件。CSV文件中的每一行都會觸發參數化測試的一次調用。
@ParameterizedTest @CsvFileSource(resources = "/two-column.csv") void testWithCsvFileSource(String first, int second) { assertNotNull(first); assertNotEquals(0, second); } 文件內容爲: foo, 1 bar, 2 "baz, qux", 3 與@CsvSource中使用的語法相反,@CsvFileSource使用雙引號"做爲引號字符,請參考上面例子中的"baz,qux"值,一個空的帶引號的值""表示一個空String,一個徹底爲空的值被當成null引用,若是null引用的目標類型是基本類型,則會拋出一個ArgumentConversionException。 |
6、@ArgumentsSource 能夠用來指定一個自定義且可以複用的ArgumentsProvider。
@ParameterizedTest @ArgumentsSource(MyArgumentsProvider.class) void testWithArgumentsSource(String argument) { assertNotNull(argument); }
static class MyArgumentsProvider implements ArgumentsProvider {
@Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) { return Stream.of("apple", "banana").map(Arguments::of); } } |
擴輾轉換
JUnit Jupiter爲提供給@ParameterizedTest的參數提供了 擴展基本類型轉換 的支持。例如,使用@ValueSource(ints = {1,2,3})註解的參數化測試能夠聲明爲不只接受int類型的參數,還接受long,float或double類型的參數。
隱式轉換
爲了支持像@CsvSource這樣的使用場景,JUnit Jupiter提供了一些內置的隱式類型轉換器。轉換過程取決於每一個方法參數的聲明類型。
例如,若是一個@ParameterizedTest方法聲明瞭TimeUnit類型的參數,而實際上提供了一個String,此時字符串會被自動轉換成對應的TimeUnit枚舉常量。
回退String-to-Object轉換
除了從字符串到上表中列出的目標類型的隱式轉換以外,若是目標類型只聲明一個合適的工廠方法 或工廠構造函數,則JUnit Jupiter還提供了一個從String自動轉換爲給定目標類型的回退機制,工廠方法和工廠構造函數定義以下:
工廠方法:在目標類型中聲明的非私有靜態方法,它接受單個String參數並返回目標類型的實例。該方法的名稱能夠是任意的,不須要遵循任何特定的約定。
工廠構造函數:目標類型中的一個非私有構造函數,它接受一個String參數。
若是發現多個工廠方法,它們將被忽略。若是同時發現了工廠方法 和工廠構造函數,則將使用工廠方法 而不使用構造函數。
顯式轉換
除了使用隱式轉換參數,你還可使用@ConvertWith註解來顯式指定一個ArgumentConverter用於某個參數,例以下面代碼所示。
@ParameterizedTest @EnumSource(TimeUnit.class) void testWithExplicitArgumentConversion(@ConvertWith(ToStringArgumentConverter.class) String argument) { assertNotNull(TimeUnit.valueOf(argument)); }
static class ToStringArgumentConverter extends SimpleArgumentConverter {
@Override protected Object convert(Object source, Class<?> targetType) { assertEquals(String.class, targetType, "Can only convert to String"); return String.valueOf(source); } } |
顯式參數轉換器意味着開發人員要本身去實現它。正由於這樣,junit-jupiter-params僅僅提供了一個能夠做爲參考實現的顯式參數轉換器:JavaTimeArgumentConverter。你能夠經過組合註解JavaTimeArgumentConverter來使用它。
@ParameterizedTest @ValueSource(strings = { "01.01.2017", "31.12.2017" }) void testWithExplicitJavaTimeConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) { assertEquals(2017, argument.getYear()); } |
默認狀況下,提供給@ParameterizedTest方法的每一個參數對應於單個方法參數。所以,指望提供大量參數的參數源可能致使大的方法簽名。
在這種狀況下,可使用ArgumentsAccessor而不是多個參數。使用此API,你能夠經過傳遞給你的測試方法的單個參數去訪問提供的參數。另外,它還支持類型轉換,如 隱式轉換中所述。
@ParameterizedTest @CsvSource({ "Jane, Doe, F, 1990-05-20", "John, Doe, M, 1990-10-22" }) void testWithArgumentsAccessor(ArgumentsAccessor arguments) { Person person = new Person(arguments.getString(0), arguments.getString(1), arguments.get(2, Gender.class), arguments.get(3, LocalDate.class));
if (person.getFirstName().equals("Jane")) { assertEquals(Gender.F, person.getGender()); } else { assertEquals(Gender.M, person.getGender()); } assertEquals("Doe", person.getLastName()); assertEquals(1990, person.getDateOfBirth().getYear()); } |
自定義聚合器
除了使用ArgumentsAccessor直接訪問@ParameterizedTest方法的參數外,JUnit Jupiter還支持使用自定義的可重用聚合器。
要使用自定義聚合器,只需實現ArgumentsAggregator接口並經過@AggregateWith註釋將其註冊到@ParameterizedTest方法的兼容參數中。當調用參數化測試時,聚合結果將做爲相應參數的參數提供。
@ParameterizedTest @CsvSource({ "Jane, Doe, F, 1990-05-20", "John, Doe, M, 1990-10-22" }) void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) { // perform assertions against person }
public class PersonAggregator implements ArgumentsAggregator { @Override public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { return new Person(arguments.getString(0), arguments.getString(1), arguments.get(2, Gender.class), arguments.get(3, LocalDate.class)); } } |
若是你發現本身在代碼庫中爲多個參數化測試方法重複聲明@AggregateWith(MyTypeAggregator.class),此時你可能但願建立一個自定義組合註解,好比@CsvToMyType,它使用@AggregateWith(MyTypeAggregator.class)進行元註解。如下示例經過自定義@CsvToPerson註解演示了這一點。
@ParameterizedTest @CsvSource({ "Jane, Doe, F, 1990-05-20", "John, Doe, M, 1990-10-22" }) void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) { // perform assertions against person }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @AggregateWith(PersonAggregator.class) public @interface CsvToPerson { } |
在參數化測試中,經過如下方法自定義顯示方法名稱。
@DisplayName("Display name of container") @ParameterizedTest(name = "{index} ==> fruit=''{0}'', rank={1}") @CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" }) void testWithCustomDisplayNames(String fruit, int rank) { } |
能夠在函數參數列表中混合使用多種參數,可是經過@ValueSource註解傳入方法中的參數必須在首位(也能夠像以前章節說的對參數使用ParameterResolver進行擴展),而下邊後邊的TestReporter就是前邊說的方法中傳入參數。
@ParameterizedTest @ValueSource(strings = "foo") void testWithRegularParameterResolver(String argument, TestReporter testReporter) { testReporter.publishEntry("argument", argument); } |
@TestTemplate 方法不是一個常規的測試用例,它是測試用例的模板。所以,它的設計初衷是用來被屢次調用,而調用次數取決於註冊提供者返回的調用上下文數量。因此,它必須結合 TestTemplateInvocationContextProvider 擴展一塊兒使用。測試模板方法每一次調用跟執行常規@Test方法同樣,它也徹底支持相同的生命週期回調和擴展。關於它的用例請參閱 爲測試模板提供調用上下文。
@RepeatedTest和@ ParameterizedTest就是固有的測試模板。
@TestFactory註解用以實現測試的動態運行。
與@Test方法相比,@TestFactory方法自己不是測試用例,而是測試用例的工廠。 所以,動態測試是工廠的產物。 從技術上講,@TestFactory方法必須返回Stream,Collection,Iterable,Iterator或DynamicNode實例數組。 DynamicNode的可實例化子類是DynamicContainer和DynamicTest。 DynamicContainer實例由顯示名稱和動態子節點列表組成,你能夠建立任意嵌套的動態節點層次結構。 DynamicTest實例將被延遲執行,從而動態甚至非肯定性地生成測試用例。
任何由@TestFactory方法返回的Stream在調用stream.close()的時候會被正確地關閉,這樣咱們就能夠安全地使用一個資源,例如:Files.lines()。
跟@Test方法同樣,@TestFactory方法不能是private或static的。但它能夠聲明被ParameterResolvers解析的參數。
DynamicTest是運行時生成的測試用例。它由一個顯示名稱 和Executable組成。Executable是一個@FunctionalInterface,這意味着動態測試的實現能夠是一個lambda表達式 或方法引用。
同一個@TestFactory所生成的n個動態測試,@BeforeEach和@AfterEach只會在這n個動態測試開始前和結束後各執行一次,不會爲每個單獨的動態測試都執行。
在JUnit Jupiter5.4.0 中,動態測試必須始終由工廠方法建立;不過,在後續的發行版中,這可能會獲得註冊工具的補充。
class DynamicTestsDemo { private final Calculator calculator = new Calculator(); // This will result in a JUnitException! @TestFactory List<String> dynamicTestsWithInvalidReturnType() { return Arrays.asList("Hello"); } @TestFactory Collection<DynamicTest> dynamicTestsFromCollection() { return Arrays.asList( dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterable<DynamicTest> dynamicTestsFromIterable() { return Arrays.asList( dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterator<DynamicTest> dynamicTestsFromIterator() { return Arrays.asList( dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ).iterator(); } @TestFactory DynamicTest[] dynamicTestsFromArray() { return new DynamicTest[] { dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) }; } @TestFactory Stream<DynamicTest> dynamicTestsFromStream() { return Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))); } @TestFactory Stream<DynamicTest> dynamicTestsFromIntStream() { // Generates tests for the first 10 even integers. return IntStream.iterate(0, n -> n + 2).limit(10) .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); } @TestFactory Stream<DynamicTest> generateRandomNumberOfTests() {
// Generates random positive integers between 0 and 100 until // a number evenly divisible by 7 is encountered. Iterator<Integer> inputGenerator = new Iterator<Integer>() {
Random random = new Random(); int current;
@Override public boolean hasNext() { current = random.nextInt(100); return current % 7 != 0; } @Override public Integer next() { return current; } }; // Generates display names like: input:5, input:37, input:85, etc. Function<Integer, String> displayNameGenerator = (input) -> "input:" + input; // Executes tests based on the current input value. ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0); // Returns a stream of dynamic tests. return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor); } @TestFactory Stream<DynamicNode> dynamicTestsWithContainers() { return Stream.of("A", "B", "C") .map(input -> dynamicContainer("Container " + input, Stream.of( dynamicTest("not null", () -> assertNotNull(input)), dynamicContainer("properties", Stream.of( dynamicTest("length > 0", () -> assertTrue(input.length() > 0)), dynamicTest("not empty", () -> assertFalse(input.isEmpty())) )) ))); } @TestFactory DynamicNode dynamicNodeSingleTest() { return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop"))); } @TestFactory DynamicNode dynamicNodeSingleContainer() { return dynamicContainer("palindromes", Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))) )); }
} |
默認狀況下,JUnit Jupiter測試在單個線程中按順序運行。要並行運行測試,例如 加速執行,自5.3版本開始做爲可選擇的功能被加入進來。要啓用並行執行,只需將junit.jupiter.execution.parallel.enabled配置參數設置爲true,例如 在junit-platform.properties中(請參閱其餘選項的 配置參數)。
啓用後,JUnit Jupiter引擎將根據提供的 配置 徹底並行地在全部級別的測試樹上執行測試,同時觀察聲明性 同步機制。 請注意,捕獲標準輸出/錯誤 功能須要單獨開啓。
⚠️ 並行測試執行目前是一項實驗性功能。 你被邀請嘗試並向JUnit團隊提供反饋,以便他們能夠 改進 並最終推廣此功能。
可使用 ParallelExecutionConfigurationStrategy 配置所需並行度和最大池大小等屬性。 JUnit平臺提供了兩種開箱即用的實現:dynamic和fixed。 固然,你也能夠實現一個custom的策略。
要選擇策略,只需將junit.jupiter.execution.parallel.config.strategy配置參數設置爲如下選項之一:
dynamic
根據可用處理器/核心數乘以junit.jupiter.execution.parallel.config.dynamic.factor配置參數(默認爲1)計算所需的並行度。
fixed 強制使用junit.jupiter.execution.parallel.config.fixed.parallelism配置參數做爲所需的並行度。
custom 容許經過強制junit.jupiter.execution.parallel.config.custom.class配置參數指定自定義 ParallelExecutionConfigurationStrategy 實現,以肯定所需的配置。
若是未設置配置任何策略,則JUnit Jupiter使用因子爲1的動態配置策略,即所需的並行度將等於可用處理器/核心的數量。
在org.junit.jupiter.api.parallel包中,JUnit Jupiter提供了兩種基於註解的聲明性機制,用於在不一樣測試中使用共享資源時更改執行模式並容許同步。
若是啓用了並行執行,默認狀況下會同時執行全部類和方法。你可使用 @Execution 註解更改帶註解的元素及其子元素(若是有)的執行模式。有如下兩種模式:
SAME_THREAD
強制執行父級使用的同一線程。例如,在測試方法上使用時,測試方法將在與包含測試類的任何@BeforeAll或@AfterAll方法相同的線程中執行。
CONCURRENT
除非存在資源約束要強制在同一線程中執行,不然執行併發。
此外,@ResourceLock 註解容許聲明測試類或測試方法使用須要同步訪問的特定共享資源,以確保可靠的測試執行。
若是你並行運行下面示例中的測試,你會發現它們很不穩定,即有時經過而其餘時間失敗。由於它們所讀取的資源在寫入是存在競爭。
class DynamicTestsDemo { private final Calculator calculator = new Calculator(); // This will result in a JUnitException! @TestFactory List<String> dynamicTestsWithInvalidReturnType() { return Arrays.asList("Hello"); } @TestFactory Collection<DynamicTest> dynamicTestsFromCollection() { return Arrays.asList( dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterable<DynamicTest> dynamicTestsFromIterable() { return Arrays.asList( dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); } @TestFactory Iterator<DynamicTest> dynamicTestsFromIterator() { return Arrays.asList( dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ).iterator(); } @TestFactory DynamicTest[] dynamicTestsFromArray() { return new DynamicTest[] { dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) }; } @TestFactory Stream<DynamicTest> dynamicTestsFromStream() { return Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))); } @TestFactory Stream<DynamicTest> dynamicTestsFromIntStream() { // Generates tests for the first 10 even integers. return IntStream.iterate(0, n -> n + 2).limit(10) .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); } @TestFactory Stream<DynamicTest> generateRandomNumberOfTests() {
// Generates random positive integers between 0 and 100 until // a number evenly divisible by 7 is encountered. Iterator<Integer> inputGenerator = new Iterator<Integer>() {
Random random = new Random(); int current;
@Override public boolean hasNext() { current = random.nextInt(100); return current % 7 != 0; } @Override public Integer next() { return current; } }; // Generates display names like: input:5, input:37, input:85, etc. Function<Integer, String> displayNameGenerator = (input) -> "input:" + input; // Executes tests based on the current input value. ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0); // Returns a stream of dynamic tests. return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor); } @TestFactory Stream<DynamicNode> dynamicTestsWithContainers() { return Stream.of("A", "B", "C") .map(input -> dynamicContainer("Container " + input, Stream.of( dynamicTest("not null", () -> assertNotNull(input)), dynamicContainer("properties", Stream.of( dynamicTest("length > 0", () -> assertTrue(input.length() > 0)), dynamicTest("not empty", () -> assertFalse(input.isEmpty())) )) ))); } @TestFactory DynamicNode dynamicNodeSingleTest() { return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop"))); } @TestFactory DynamicNode dynamicNodeSingleContainer() { return dynamicContainer("palindromes", Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))) )); }
} |
當使用該註解聲明對共享資源的訪問時,JUnit Jupiter引擎會使用此信息來確保不會並行運行衝突的測試。
除了用於惟一標記已使用資源的字符串以外,你還能夠指定訪問模式。須要對資源進行READ訪問的兩個測試能夠彼此並行運行,除非有其餘READ_WRITE訪問模式的測試正在運行。
JUnit團隊鼓勵開發者開發擴展,這裏介紹一個JUnit已經含有的一個擴展。
@TempDir,實現這個註解具體功能的類網址爲https://github.com/junit-team/junit5/blob/r5.4.0/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java
(能夠經過實現的代碼看到他繼承了ParameterResolver,這個又是參數注入的內容,咱們實現自定義註解都要這個樣子哦),這個註解的做用是獲取當前工做目錄。
@Test void writeItemsToFile(@TempDir Path tempDir) throws IOException { System.out.println(tempDir); Path file = tempDir.resolve("/home/user/Coding/JavaCode/SSM/src/test/java/TestJunit5/test.txt");
// new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file)); } |
@TempDir不能放在構造方法上。
下邊是一個例子,他還能夠標註在靜態屬性上。
class SharedTempDirectoryDemo {
@TempDir static Path sharedTempDir;
@Test void writeItemsToFile() throws IOException { Path file = sharedTempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file)); }
@Test void anotherTestThatUsesTheSameTempDir() { // use sharedTempDir }
} |
參數配置在原用戶手冊的4.5章節中https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params。
能夠經過給jvm傳參或者在根目錄添加junit-platform.properties文件等方法實現JUnit的參數配置。
這一章節的內容就是如何爲Junit添加本身定義的註解,本身定義的參數解析,實現更多的本身須要的功能。
JUnit Jupiter中的擴展能夠經過 @ExtenWith 註解進行聲明式註冊,或者經過 @RegisterExtension 註解進行編程式註冊,再或者經過Java的 ServiceLoader 機制自動註冊。
這一章節全部的類都是與 Extension接口有關,能夠發現繼承它的一些接口是分別對應不一樣的擴展,經過實現這些接口來實現咱們須要的功能。
能夠經過@ExtendWith傳入類來進行擴展。能夠傳入多個的方法以下。能夠看到這個註解的功能應該是能夠引入擴展類,而後就能夠得到額外的功能,@Random註解是框架所沒有的。
//@ExtendWith({ FooExtension.class, BarExtension.class }) //@ExtendWith(FooExtension.class) //@ExtendWith(BarExtension.class) @ExtendWith(RandomParametersExtension.class) @Test void test(@Random int i) { // ... } |
開發人員能夠經過編程的 方式來註冊擴展,只須要將測試類中的屬性字段使用 @RegisterExtension 註解標註便可。
當一個擴展經過 @ExtenWith 聲明式註冊後,它就只能經過註解配置。相比之下,當經過@RegisterExtension註冊擴展時,咱們能夠經過編程 的方式來配置擴展 – 例如,將參數傳遞給擴展的構造函數、靜態工廠方法或構建器API。
@RegisterExtension 字段不能爲private或null (在評估階段) ,但能夠是static或非靜態。
靜態字段
若是一個@RegisterExtension字段是static的,該擴展會在那些在測試類中經過@ExtendWith進行註冊的擴展以後被註冊。這種靜態擴展 在擴展API的實現上沒有任何限制。所以,經過靜態字段註冊的擴展可能會實現類級別和實例級別的擴展API,例如BeforeAllCallback、AfterAllCallback和TestInstancePostProcessor,一樣還有方法級別的擴展API,例如BeforeEachCallback等等。
在下面的例子中,測試類中的server字段經過使用WebServerExtension所支持的構建器模式以編程的方式進行初始化。已經配置的WebServerExtension將在類級別自動註冊爲一個擴展 - 例如,要在測試類中全部測試方法運行以前啓動服務器,以及在全部測試完成後中止服務器。此外,使用@BeforeAll或@AfterAll標註的靜態生命週期方法以及@BeforeEach、@AfterEach和@Test標註的方法能夠在須要的時候經過server字段訪問該擴展的實例。
一個經過靜態字段註冊的擴展:
class WebServerDemo {
@RegisterExtension static WebServerExtension server = WebServerExtension.builder() .enableSecurity(false) .build();
@Test void getProductList() { WebClient webClient = new WebClient(); String serverUrl = server.getServerUrl(); // Use WebClient to connect to web server using serverUrl and verify response assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus()); } } |
實例字段
若是@RegisterExtension字段是非靜態的(例如,一個實例字段),那麼該擴展將在測試類實例化以後被註冊,而且在每一個已註冊的TestInstancePostProcessor被賦予後處理測試實例的機會以後(可能給被標註的字段注入要使用的擴展實例)。所以,若是這樣的實例擴展實現了諸如BeforeAllCallback、AfterAllCallback或TestInstancePostProcessor這些類級別或實例級別的擴展API,那麼這些API將不會正常執行。默認狀況下,實例擴展將在那些經過@ExtendWith在方法級別註冊的擴展以後被註冊。可是,若是測試類是使用了@TestInstance(Lifecycle.PER_CLASS)配置,實例擴展將在它們以前被註冊。
在下面的例子中,經過調用自定義lookUpDocsDir()方法並將結果提供給DocumentationExtension中的靜態forPath()工廠方法,從而以編程的方式初始化測試類中的docs字段。配置的DocumentationExtension將在方法級別自動被註冊爲擴展。另外,@BeforeEach、@AfterEach和@Test方法能夠在須要的時候經過docs字段訪問擴展的實例。
一個經過靜態字段註冊的擴展:
class DocumentationDemo {
static Path lookUpDocsDir() { // return path to docs dir }
@RegisterExtension DocumentationExtension docs = DocumentationExtension.forPath(lookUpDocsDir());
@Test void generateDocumentation() { // use this.docs ... } } |
除了 聲明式擴展註冊 和 編程式擴展註冊 支持使用註解,JUnit Jupiter還支持經過Java的java.util.ServiceLoader機制進行全局擴展註冊,採用這種機制後會自動的檢測classpath下的第三方擴展,並自動完成註冊。
具體來講,自定義擴展能夠經過在org.junit.jupiter.api.extension.Extension文件中提供其全類名來完成註冊,該文件位於其封閉的JAR文件中的/META-INF/services目錄下。
啓用自動擴展檢測
自動檢測是一種高級特性,默認狀況下它是關閉的。要啓用它,只須要在配置文件中將 junit.jupiter.extensions.autodetection.enabled的配置參數 設置爲 true便可。該參數能夠做爲JVM系統屬性、或做爲一個傳遞給Launcher的LauncherDiscoveryRequest中的配置參數、再或者經過JUnit Platform配置文件(詳情請參閱 配置參數)來提供。
例如,要啓用擴展的自動檢測,你能夠在啓動JVM時傳入以下系統參數。
-Djunit.jupiter.extensions.autodetection.enabled=true
啓用自動檢測功能後,經過ServiceLoader機制發現的擴展將在JUnit Jupiter的全局擴展(例如對TestInfo,TestReporter等的支持)以後被添加到擴展註冊表中。
ExecutionCondition 定爲程序化的條件測試執行定義了ExtensionAPI。
這裏爲了搞明白怎麼自定義擴展,特別是條件擴展,能夠參考以下的DisabledOnOs是如何實現的。在註解中經過@ExtendWith()添加判斷狀況的類,DisabledOnOsCondition是繼承了ExecutionCondition用以實現條件判斷,根據代碼能夠看出,在方法中首先嚐試獲取註解,而後判斷註解在不在,在的狀況下在判斷輸入的值(操做系統)是否符合要求。
簡單說明一下@Disabled註解的註解判斷條件類DisabledCondition類在junit-jupiter-engine中。
TestInstanceFactory 爲但願建立測試類實例的Extensions定義了API。
TestInstancePostProcessor 爲但願發佈流程測試實例的Extensions定義了API。
關於具體示例,請查閱 MockitoExtension 和 SpringExtension 的源代碼。
ParameterResolver 定義了用於在運行時動態解析參數的ExtensionAPI。
爲了實現傳入自定義的參數,咱們要實現對應的參數解析器。能夠參照TestInfoParameterResolver這個類來寫參數解析器,這個解析器解析的是在以前章節提到過的三種Junit自帶的參數解析器,對應解析TestInfo類,能夠在測試方法中傳入TestInfo參數獲取對應測試的相關信息。
TestWatcher提供API以實現擴展用以處理測試方法的結果。經過看這個類的註釋,大概知道這個接口用來提供測試方法被跳過或者被執行等結果進行報告。
其中四種方法分別對應
testDisabled: 被@Disabled註釋的方法跳事後
testSuccessful: 成功測試
testAborted:測試終止
testFailed: 測試失敗
下列接口定義了用於在測試執行生命週期的不一樣階段來擴展測試的API。關於每一個接口的詳細信息,能夠參考後續章節的示例,也能夠查閱 org.junit.jupiter.api.extension 包中的Javadoc。
AfterAllCallback
擴展開發人員能夠選擇在單個擴展中實現任意數量的上述接口。
BeforeTestExecutionCallback 和 AfterTestExecutionCallback 分別爲Extensions定義了添加行爲的API,這些行爲將在執行測試方法以前 和以後當即執行。所以,這些回調很是適合於定時器、跟蹤器以及其餘相似的場景。若是你須要實現圍繞@BeforeEach和@AfterEach方法調用的回調,實現BeforeEachCallback和AfterEachCallback便可。
如下示例展現瞭如何使用這些回調來統計和記錄測試方法的執行時間。TimingExtension同時實現了BeforeTestExecutionCallback和AfterTestExecutionCallback接口,從而給測試執行進行計時和記錄。
一個爲測試方法執行計時和記錄的擴展
import java.lang.reflect.Method; import java.util.logging.Logger; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store;
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final Logger LOG = Logger.getLogger(TimingExtension.class.getName()); @Override public void beforeTestExecution(ExtensionContext context) throws Exception { getStore(context).put(context.getRequiredTestMethod(), System.currentTimeMillis()); } @Override public void afterTestExecution(ExtensionContext context) throws Exception { Method testMethod = context.getRequiredTestMethod(); long start = getStore(context).remove(testMethod, long.class); long duration = System.currentTimeMillis() - start; LOG.info(() -> String.format("Method [%s] took %s ms.", testMethod.getName(), duration)); } private Store getStore(ExtensionContext context) { return context.getStore(Namespace.create(getClass(), context)); } } |
因爲TimingExtensionTests類經過@ExtendWith註冊了TimingExtension,因此,測試將在執行時應用這個計時器。
一個使用示例TimingExtension的測試類
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(TimingExtension.class) class TimingExtensionTests { @Test void sleep20ms() throws Exception { Thread.sleep(20); } @Test void sleep50ms() throws Exception { Thread.sleep(50); } } |
測試結果以下圖所示。
TestExecutionExceptionHandler 爲Extensions定義了異常處理的API,從而能夠處理在執行測試時拋出的異常。
當至少有一個 TestTemplateInvocationContextProvider 被註冊時,標註了 @TestTemplate 的方法才能被執行。每一個這樣的provider負責提供一個 TestTemplateInvocationContext 實例的Stream。每一個上下文均可以指定一個自定義的顯示名稱和一個額外的擴展名列表,這些擴展名僅用於下一次調用 @TestTemplate 方法。
如下示例展現瞭如何編寫測試模板以及如何註冊和實現一個 TestTemplateInvocationContextProvider。
public class MyTestTemplateInvocationContextProviderTest { final List<String> fruits = Arrays.asList("apple", "banana", "lemon"); //這裏的做用是想將字符串對象傳入String fruit中 //所謂測試模板確定是要能傳入一系列的參數,因此在具體實現中是要返回一個stream對象 @TestTemplate @ExtendWith(MyTestTemplateInvocationContextProvider.class) void testTemplate(String fruit) { assertTrue(fruits.contains(fruit)); } } |
//TestTemplateInvocationContextProvider繼承這個接口以實現測試模板 public class MyTestTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider { //是否支持測試模板,true爲開啓支持 @Override public boolean supportsTestTemplate(ExtensionContext context) { return true; }
@Override public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts( ExtensionContext context) {
return Stream.of(invocationContext("apple"), invocationContext("banana")); } //這裏選擇含兩個元素的stream流,須要將對應的String類型轉換爲TestTemplateInvocationContext類型 private TestTemplateInvocationContext invocationContext(String parameter) { return new TestTemplateInvocationContext() { //這裏將傳入的String參數設置爲測試中的DisplayName @Override public String getDisplayName(int invocationIndex) { return parameter; }
@Override public List<Extension> getAdditionalExtensions() { return Collections.singletonList(new ParameterResolver() { //判斷是不是//判斷是不是String類型String類型 @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return parameterContext.getParameter().getType().equals(String.class); } //把String類型參數直接返回,咱們須要解析的就是String類型,若是是別的類型確定要涉及轉換 @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return parameter; } }); } }; } } |
以前講過測試模板的例子,@RepeatedTest和@ParameterizedTest就是兩個典型,咱們若是要寫本身的測試模板能夠參照這兩個例子的實現,簡單看下。
能夠看到@RepeatedTest的確是包含了@TestTemplate。
而後具體的註解處理類在junit-platform-engine中的RepeatedTestExtension類中,能夠看到熟悉的要重寫的方法,第一個supportsTestTemplate方法中的邏輯是「方法是否被RepeatedTest註解標註」,第二個provideTestTemplateInvocationContexts方法中的邏輯是「返回IntStream流,由於@RepeatedTest的註解裏邊參數是傳的int類型」。
一般,擴展只實例化一次。隨之而來的相關問題是:開發者如何可以在兩次調用之間保持擴展的狀態?ExtensionContext API提供了一個Store用來解決這一問題(參見測試生命週期回調那個例子)。擴展能夠將值放入Store中供之後檢索。請參閱 TimingExtension 瞭解如何使用具備方法級做用域的Store。要注意,在測試執行期間,被存儲在一個ExtensionContext中的值在周圍其餘的ExtensionContext中是不可用的。因爲ExtensionContexts多是嵌套的,所以內部上下文的範圍也可能受到限制。請參閱相應的Javadoc來了解有關經過 Store 存儲和檢索值的方法的詳細信息。
junit-platform-commons公開了一個名爲 的包,它包含了用於處理註解、類、反射和類路徑掃描任務且正在維護中的實用工具方法。TestEngine和Extension開發人員(authors)應該被鼓勵去使用這些方法,以便與JUnit Platform的行爲保持一致。
AnnotationSupport提供對註解元素(例如包、註解、類、接口、構造函數、方法和字段)進行操做的靜態實用工具方法。這些方法包括檢查元素是否使用特定註釋進行註解或元註解,搜索特定註解以及如何在類或界面中查找註解的方法和字段。其中一些方法搜索已實現的接口和類層次結構以查找註解。有關更多詳細信息,請參閱JavaDoc的 AnnotationSupport。
ClassSupport提供靜態工具方法來處理類(即java.lang.Class的實例)。有關詳細信息,請參閱JavaDoc的 ClassSupport。
ReflectionSupport提供了靜態實用工具方法,以加強標準的JDK反射和類加載機制。這些方法包括掃描類路徑以搜索匹配了指定謂詞的類,加載和建立類的新實例以及查找和調用方法。其中一些方法能夠遍歷類層次結構以找到匹配的方法。有關更多詳細信息,請參閱JavaDoc的 ReflectionSupport。
ModifierSupport provides static utility methods for working with member and class modifiers — for example, to determine if a member is declared as public, private, abstract, static, etc. Consult the Javadoc for ModifierSupport for further details.
當執行包含一個或多個測試方法的測試類時,除了用戶提供的測試和生命週期方法外,還會調用大量的回調函數。 下圖說明了用戶提供的代碼和擴展代碼的相對順序。
用戶代碼和擴展代碼
用戶提供的測試和生命週期方法以橙色表示,擴展提供的回調代碼由藍色顯示。灰色框表示單個測試方法的執行,並將在測試類中對每一個測試方法重複執行。
下表進一步解釋了 用戶代碼和擴展代碼 圖中的十二個步驟。
步驟 |
接口/註解 |
描述 |
1 |
接口org.junit.jupiter.api.extension.BeforeAllCallback |
執行全部容器測試以前執行的擴展代碼 |
2 |
註解org.junit.jupiter.api.BeforeAll |
執行全部容器測試以前執行的用戶代碼 |
3 |
接口org.junit.jupiter.api.extension.BeforeEachCallback |
每一個測試執行以前執行的擴展代碼 |
4 |
註解org.junit.jupiter.api.BeforeEach |
每一個測試執行以前執行的用戶代碼 |
5 |
接口org.junit.jupiter.api.extension.BeforeTestExecutionCallback |
測試執行以前當即執行的擴展代碼 |
6 |
註解org.junit.jupiter.api.Test |
真實測試方法的用戶代碼 |
7 |
接口org.junit.jupiter.api.extension.TestExecutionExceptionHandler |
用於處理測試期間拋出的異常的擴展代碼 |
8 |
接口org.junit.jupiter.api.extension.AfterTestExecutionCallback |
測試執行後當即執行的擴展代碼 |
9 |
註解org.junit.jupiter.api.AfterEach |
每一個執行測試以後執行的用戶代碼 |
10 |
接口org.junit.jupiter.api.extension.AfterEachCallback |
每一個執行測試以後執行的擴展代碼 |
11 |
註解org.junit.jupiter.api.AfterAll |
執行全部容器測試以後執行的用戶代碼 |
12 |
接口org.junit.jupiter.api.extension.AfterAllCallback |
執行全部容器測試以後執行的擴展代碼 |
在最簡單的狀況下,只有實際的測試方法被執行(步驟6); 全部其餘步驟都是可選的,具體包含的步驟將取決因而否存在用戶代碼或對相應生命週期回調的擴展支持。有關各類生命週期回調的更多詳細信息,請參閱每一個註解和擴展各自的JavaDoc。
對應https://junit.org/junit5/docs/current/user-guide/#extensions-execution-order-wrapping-behavior 章節,大體就是講不一樣自定義擴展被加入以後的執行順序和涉及類繼承關係時的測試執行順序。