轉自:https://www.ibm.com/developerworks/cn/java/j-introducing-junit5-part1-jupiter-api/index.htmlhtml
https://www.ibm.com/developerworks/cn/java/j-introducing-junit5-part2-vintage-jupiter-extension-model/index.htmljava
第 1 部分git
瞭解全新 JUnit Jupiter API 中的註解、斷言和前置條件github
本教程介紹 JUnit 5。咱們首先介紹如何在您的計算機上安裝並設置 JUnit 5。我將簡要介紹 JUnit 5 的架構和組件,而後展現如何使用 JUnit Jupiter API 中的新註解、斷言和前置條件。apache
在第 2 部分中,咱們將更深刻地介紹 JUnit 5,包括新的 JUnit Jupiter 擴展模型、參數注入、動態測試等。api
在本教程中,我使用了 JUnit 5, Milestone 5。數組
出於本教程的目的,我假設您熟悉如下軟件的使用:安全
要跟隨示例進行操做,您應在計算機上安裝 JDK 八、Eclipse、Maven、Gradle(可選)和 Git。若是缺乏其中的任何工具,可以使用下面的連接下載和安裝它們:架構
人們傾向於將術語 JUnit 5 和 JUnit Jupiter 看成同義詞使用。在大部分狀況下,這種互換使用沒有什麼問題。可是,必定要認識到這兩個術語是不一樣的。JUnit Jupiter 是使用 JUnit 5 編寫測試內容的 API。JUnit 5 是一個項目名稱(和版本),其 3 個主要模塊關注不一樣的方面:JUnit Jupiter、JUnit Platform 和 JUnit Vintage。
當我說起 JUnit Jupiter 時,指的是編寫單元測試的 API;說起 JUnit 5 時,指的是整個項目。
之前的 JUnit 版本都是總體式的。除了在 4.4 版中包含 Hamcrest JAR,JUnit 基原本講就是一個很大的 JAR 文件。測試內容編寫者 — 像您我這樣的開發人員 — 和工具供應商都使用它的 API,但後者使用不少內部 JUnit API。
大量使用內部 API 給 JUnit 的維護者形成了一些麻煩,而且留給他們推進該技術發展的選擇餘地很少。來自 JUnit 5 用戶指南:
「在 JUnit 4 中,只有外部擴展編寫者和工具構建者才使用最初做爲內部結構而添加的許多功能。這讓更改 JUnit 4 變得特別困難,有時甚至根本不可能。」
JUnit Lambda(如今稱爲 JUnit 5)團隊決定將 JUnit 從新設計爲兩個明確且不一樣的關注區域:
這些關注區域如今已整合到 JUnit 5 的架構中,而且它們是明確分離的。圖 1 演示了新架構(圖像來自 Nicolai Parlog):
若是仔細查看圖 1,就會發現 JUnit 5 的架構有多麼強大。好了,讓咱們仔細看看這個架構。右上角的方框代表,對 JUnit 5 而言,JUnit Jupiter API 只是另外一個 API!由於 JUnit Jupiter 的組件遵循新的架構,因此它們可應用 JUnit 5,但您能夠輕鬆定義不一樣的測試框架。只要一個框架實現了 TestEngine
接口,就能夠將它插入任何支持 junit-platform-engine
和 junit-platform-launcher
API 的工具中!
我仍然認爲 JUnit Jupiter 很是特殊(畢竟我即將用一整篇教程來介紹它),但 JUnit 5 團隊完成的工做確實具備開創性。我只是想指出這一點。咱們繼續看看圖 1,直到咱們徹底達成一致。
就測試編寫者而言,任何符合 JUnit 規範的測試框架(包括 JUnit Jupiter)都包含兩個組件:
TestEngine
實現。對於本教程,前者是 JUnit Jupiter API,後者是 JUnit Jupiter Test Engine。我將介紹這兩者。
做爲開發人員,您將使用 JUnit Jupiter API 建立單元測試來測試您的應用程序代碼。使用該 API 的基本特性 — 註解、斷言等 — 是本部分教程的主要關注點。
JUnit Jupiter API 的設計讓您可經過插入各類生命週期回調來擴展它的功能。您將在第 2 部分中瞭解如何使用這些回調完成有趣的工做,好比運行參數化測試,將參數傳遞給測試方法,等等。
您將使用 JUnit Jupiter Test Engine 發現和執行 JUnit Jupiter 單元測試。該測試引擎實現了 JUnit Platform 中包含的 TestEngine
接口。可將 TestEngine
看做單元測試與用於啓動它們的工具(好比 IDE)之間的橋樑。
在 JUnit 術語中,運行單元測試的過程分爲兩部分:
用於發現測試和建立測試計劃的 API 包含在 JUnit Platform 中,由一個 TestEngine
實現。該測試框架將測試發現功能封裝到其 TestEngine
實現中。JUnit Platform 負責使用 IDE 和構建工具(好比 Gradle 和 Maven)發起測試發現流程。
測試發現的目的是建立測試計劃,該計劃中包含一個測試規範。測試規範包含如下組件:
測試計劃是根據測試規範所發現的全部測試類、這些類中的測試方法、測試引擎等的分層視圖。測試計劃準備就緒後,就能夠執行了。
用於執行測試的 API 包含在 JUnit Platform 中,由一個或多個 TestEngine
實現。測試框架將測試執行功能封裝在它們的 TestEngine
實現中,但 JUnit Platform 負責發起測試執行流程。經過 IDE 和構建工具(好比 Gradle 和 Maven)發起測試執行工做。
一個名爲 Launcher
的 JUnit Platform 組件負責執行在測試發現期間建立的測試計劃。某個流程 — 假設是您的 IDE — 經過 JUnit Platform(具體來說是 junit-platform-launcher
API)發起測試執行流程。這時,JUnit Platform 將測試計劃連同 TestExecutionListener
一塊兒傳遞給 Launcher
。TestExecutionListener
將報告測試執行結果,從而在您的 IDE 中顯示該結果。
測試執行流程的目的是向用戶準確報告在測試運行時發生了哪些事件。這包括測試成功和失敗報告,以及伴隨失敗而生成的消息,幫助用戶理解所發生的事件。
許多組織對 JUnit 3 和 4 進行了大力投資,所以沒法承擔向 JUnit 5 的大規模轉換。瞭解到這一點後,JUnit 5 團隊提供了junit-vintage-engine
和 junit-jupiter-migration-support
組件來幫助企業進行遷移。
對 JUnit Platform 而言,JUnit Vintage 只是另外一個測試框架,包含本身的 TestEngine
和 API(具體來說是 JUnit 4 API)。
圖 2 顯示了各類 JUnit 5 包之間的依賴關係。
支持 JUnit 的測試框架在如何處理測試執行期間拋出的異常方面有所不一樣。JVM 上的測試沒有統一標準,這是 JUnit 團隊一直要面對的問題。除了 java.lang.AssertionError
,測試框架還必須定義本身的異常分層結構,或者將自身與 JUnit 支持的異常結合起來(或者在某些狀況下同時採起兩種方法)。
爲了解決一致性問題,JUnit 團隊提議創建一個開源項目,該項目目前稱爲 Open Test Alliance for the JVM(JVM 開放測試聯盟)。該聯盟在此階段僅是一個提案,它僅定義了初步的異常分層結構。可是,JUnit 5 使用 opentest4j
異常。(可在圖 2 中看到這一點;請注意從 junit-jupiter-api
和 junit-platform-engine
包到 opentest4j
包的依賴線。)
如今您已基本瞭解各類 JUnit 5 組件如何結合在一塊兒,是時候使用 JUnit Jupiter API 編寫一些測試了!
從 JUnit 4 開始,註解 (annotation) 就成爲測試框架的核心特性,這一趨勢在 JUnit 5 中得以延續。我沒法介紹 JUnit 5 的全部註解,本節僅簡要介紹最經常使用的註解。
首先,我將比較 JUnit 4 中與 JUnit 5 中的註解。JUnit 5 團隊更改了一些註解的名稱,讓它們更直觀,同時保持功能不變。若是您正在使用 JUnit 4,下表將幫助您適應這些更改。
接下來看看一些使用這些註解的示例。儘管一些註解已在 JUnit 5 中重命名,但若是您使用過 JUnit 4,應熟悉它們的功能。清單 1 中的代碼來自 JUnit5AppTest.java
,可在 HelloJUnit5 示例應用程序中找到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
@RunWith(JUnitPlatform.class)
@DisplayName("Testing using JUnit 5")
public class JUnit5AppTest {
private static final Logger log = LoggerFactory.getLogger(JUnit5AppTest.class);
private App classUnderTest;
@BeforeAll
public static void init() {
// Do something before ANY test is run in this class
}
@AfterAll
public static void done() {
// Do something after ALL tests in this class are run
}
@BeforeEach
public void setUp() throws Exception {
classUnderTest = new App();
}
@AfterEach
public void tearDown() throws Exception {
classUnderTest = null;
}
@Test
@DisplayName("Dummy test")
void aTest() {
log.info("As written, this test will always pass!");
assertEquals(4, (2 + 2));
}
@Test
@Disabled
@DisplayName("A disabled test")
void testNotRun() {
log.info("This test will not run (it is disabled, silly).");
}
.
.
}
|
看看上面突出顯示行中的註解:
@RunWith
連同它的參數 JUnitPlatform.class
(一個基於 JUnit 4 且理解 JUnit Platform 的 Runner
)讓您能夠在 Eclipse 內運行 JUnit Jupiter 單元測試。Eclipse 還沒有原生支持 JUnit 5。將來,Eclipse 將提供原生的 JUnit 5 支持,那時咱們再也不須要此註解。@DisplayName
告訴 JUnit 在報告測試結果時顯示 String
「Testing using JUnit 5」,而不是測試類的名稱。@BeforeAll
告訴 JUnit 在運行這個類中的全部 @Test
方法以前運行 init()
方法一次。@AfterAll
告訴 JUnit 在運行這個類中的全部 @Test
方法以後運行 done()
方法一次。@BeforeEach
告訴 JUnit 在此類中的每一個@Test
方法以前運行 setUp()
方法。@AfterEach
告訴 JUnit 在此類中的每一個@Test
方法以後運行 tearDown()
方法。@Test
告訴 JUnit,aTest()
方法是一個 JUnit Jupiter 測試方法。@Disabled
告訴 JUnit 不運行此 @Test
方法,由於它已被禁用。 斷言 (assertion) 是 org.junit.jupiter.api.Assertions
類上的衆多靜態方法之一。斷言用於測試一個條件,該條件必須計算爲 true
,測試才能繼續執行。
若是斷言失敗,測試會在斷言所在的代碼行上中止,並生成斷言失敗報告。若是斷言成功,測試會繼續執行下一行代碼。
表 2 中列出的全部 JUnit Jupiter 斷言方法都接受一個可選的 message
參數(做爲最後一個參數),以顯示斷言是否失敗,而不是顯示標準的缺省消息。
清單 2 給出了一個使用這些斷言的示例,該示例來自 HelloJUnit5 示例應用程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
.
.
@Test
@DisplayName("Dummy test")
void dummyTest() {
int expected = 4;
int actual = 2 + 2;
assertEquals(expected, actual, "INCONCEIVABLE!");
//
Object nullValue = null;
assertFalse(nullValue != null);
assertNull(nullValue);
assertNotNull("A String", "INCONCEIVABLE!");
assertTrue(nullValue == null);
.
.
}
|
看看上面突出顯示行中的斷言:
assertEquals
:若是第一個參數值 (4) 不等於第二個參數值 (2+2),則斷言失敗。在報告斷言失敗時使用用戶提供的消息(該方法的第 3 個參數)。assertFalse
:表達式 nullValue != null
必須爲 false
,不然斷言失敗。assertNull
:nullValue
參數必須爲 null
,不然斷言失敗。assertNotNull
:String
文字值 「A String」 不得爲 null
,不然斷言失敗並報告消息 「INCONCEIVABLE!」(而不是缺省的 「Assertion failed」 消息)。assertTrue
:若是表達式 nullValue == null
不等於 true
,則斷言失敗。除了支持這些標準斷言,JUnit Jupiter AP 還提供了多個新斷言。下面介紹其中的兩個。
清單 3 中的 @assertAll()
方法給出了清單 2 中看到的相同斷言,但包裝在一個新的斷言方法中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import static org.junit.jupiter.api.Assertions.assertAll;
.
.
@Test
@DisplayName("Dummy test")
void dummyTest() {
int expected = 4;
int actual = 2 + 2;
Object nullValue = null;
.
.
assertAll(
"Assert All of these",
() -> assertEquals(expected, actual, "INCONCEIVABLE!"),
() -> assertFalse(nullValue != null),
() -> assertNull(nullValue),
() -> assertNotNull("A String", "INCONCEIVABLE!"),
() -> assertTrue(nullValue == null));
}
|
assertAll()
的有趣之處在於,它包含的全部斷言都會執行,即便一個或多個斷言失敗也是如此。與此相反,在清單 2 中的代碼中,若是任何斷言失敗,測試就會在該位置失敗,意味着不會執行任何其餘斷言。
在某些條件下,接受測試的類應拋出異常。JUnit 4 經過 expected =
方法參數或一個 @Rule
提供此能力。與此相反,JUnit Jupiter 經過 Assertions
類提供此能力,使它與其餘斷言更加一致。
咱們將所預期的異常視爲能夠進行斷言的另外一個條件,所以 Assertions
包含處理此條件的方法。清單 4 引入了新的assertThrows()
斷言方法。
1
2
3
4
5
6
7
8
9
10
|
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
.
.
@Test()
@DisplayName("Empty argument")
public void testAdd_ZeroOperands_EmptyArgument() {
long[] numbersToSum = {};
assertThrows(IllegalArgumentException.class, () -> classUnderTest.add(numbersToSum));
}
|
請注意第 9 行:若是對 classUnderTest.add()
的調用沒有拋出 IllegalArgumentException
,則斷言失敗。
前置條件 (Assumption) 與斷言相似,但前置條件必須爲 true,不然測試將停止。與此相反,當斷言失敗時,則將測試視爲已失敗。測試方法只應在某些條件 —前置條件下執行時,前置條件頗有用。
前置條件是 org.junit.jupiter.api.Assumptions
類的靜態方法。要理解前置條件的價值,只需一個簡單的示例。
假如您只想在星期五運行一個特定的單元測試(我假設您有本身的理由):
1
2
3
4
5
6
7
|
@Test
@DisplayName("This test is only run on Fridays")
public void testAdd_OnlyOnFriday() {
LocalDateTime ldt = LocalDateTime.now();
assumeTrue(ldt.getDayOfWeek().getValue() == 5);
// Remainder of test (only executed if assumption holds)...
}
|
在此狀況下,若是條件不成立(第 5 行),就不會執行 lambda 表達式的內容。
請注意第 5 行:若是該條件不成立,則跳過該測試。在此狀況下,該測試不是在星期五 (5) 運行的。這不會影響項目的 「綠色」 部分,並且不會致使構建失敗;會跳過 assumeTrue()
後的測試方法中的全部代碼。
若是在前置條件成立時僅應執行測試方法的一部分,可使用 assumingThat()
方法編寫上述條件,該方法使用 lambda 語法:
1
2
3
4
5
6
7
8
9
10
|
@Test
@DisplayName("This test is only run on Fridays (with lambda)")
public void testAdd_OnlyOnFriday_WithLambda() {
LocalDateTime ldt = LocalDateTime.now();
assumingThat(ldt.getDayOfWeek().getValue() == 5,
() -> {
// Execute this if assumption holds...
});
// Execute this regardless
}
|
注意,不管 assumingThat()
中的前置條件成立與否,都會執行 lambda 表達式後的全部代碼。
在繼續介紹下節內容以前,我想介紹在 JUnit 5 中編寫單元測試的最後一個特性。
JUnit Jupiter API 容許您建立嵌套的類,以保持測試代碼更清晰,這有助於讓測試結果更易讀。經過在主類中建立嵌套的測試類,能夠建立更多的名稱空間,這提供了兩個主要優點:
testMethodButOnlyUnderThisOrThatCondition_2()
的方法名)。從 JUnit Jupiter 開始,只有嵌套類中的方法必須具備惟一的名稱。清單 6 展現了這一優點。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@RunWith(JUnitPlatform.class)
@DisplayName("Testing JUnit 5")
public class JUnit5AppTest {
.
.
@Nested
@DisplayName("When zero operands")
class JUnit5AppZeroOperandsTest {
// @Test methods go here...
}
.
.
}
|
請注意第 6 行,其中的 JUnit5AppZeroOperandsTest
類能夠擁有測試方法。任何測試的結果都會在父類 JUnit5AppTest
中以嵌套的形式顯示。
能編寫單元測試很不錯,但若是不能運行它們,就沒有什麼意義了。本節展現如何在 Eclipse 中運行 JUnit 測試,首先使用 Maven,而後從命令行使用 Gradle。
下面的視頻展現瞭如何從 GitHub 克隆示例應用程序代碼,並在 Eclipse 中運行測試。在該視頻中,我還展現瞭如何從命令行以及 Eclipse 內使用 Maven 和 Gradle 運行單元測試。Eclipse 對 Maven 和 Gradle 都提供了很好的支持。
應用 3 種工具運行單元測試
下面將提供一些簡要的說明,但該視頻提供了更多細節。觀看該視頻,瞭解如何:
要理解教程的剩餘部分,您須要從 GitHub 克隆示例應用程序。爲此,可打開一個終端窗口 (Mac) 或命令提示 (Windows),導航到您但願放入代碼的目錄,而後輸入如下命令:
git clone https://github.com/makotogo/HelloJUnit5
|
如今您的機器上已擁有該代碼,能夠在 Eclipse IDE 內運行 JUnit 測試了。接下來介紹如何運行測試。
若是您已跟隨該視頻進行操做,應該已將代碼導入 Eclipse 中。如今,在 Eclipse 中打開 Project Explorer 視圖,展開 HelloJUnit5 項目,直至看到 src/test/java
路徑下的 JUnit5AppTest
類。
打開 JUnit5AppTest.java
並驗證 class
定義前的下面這個註解(如下代碼的第 3 行):
1
2
3
4
5
6
7
|
.
.
@RunWith(JUnitPlatform.class)
public class JUnit5AppTest {
.
.
}
|
如今右鍵單擊 JUnit5AppTest
並選擇 Run As > JUnit Test。單元測試運行時,JUnit 視圖將會出現。您如今已準備好完成本教程的練習。
打開一個終端窗口 (Mac) 或命令提示 (Windows),導航到您將 HelloJUnit5 應用程序克隆到的目錄,而後輸入如下命令:
mvn test
|
這會啓動 Maven 構建並運行單元測試。您的輸出應相似於:
$ mvn test
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building HelloJUnit5 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ HelloJUnit5 ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/sperry/home/projects/learn/HelloJUnit5/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.6.1:compile (default-compile) @ HelloJUnit5 ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ HelloJUnit5 ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/sperry/home/projects/learn/HelloJUnit5/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.6.1:testCompile (default-testCompile) @ HelloJUnit5 ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.19:test (default-test) @ HelloJUnit5 ---
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.makotojava.learn.hellojunit5.JUnit5AppTest
17:08:56.137 [main] INFO com.makotojava.learn.hellojunit5.JUnit5AppTest - As written, this test will always pass!
Tests run: 2, Failures: 0, Errors: 0, Skipped: 1, Time elapsed: 0.112 sec - in com.makotojava.learn.hellojunit5.JUnit5AppTest
Running com.makotojava.learn.hellojunit5.solution.JUnit5AppTest
17:08:56.166 [main] INFO com.makotojava.learn.hellojunit5.solution.JUnit5AppTest - As written, this test will always pass!
Tests run: 11, Failures: 0, Errors: 0, Skipped: 2, Time elapsed: 0.052 sec - in com.makotojava.learn.hellojunit5.solution.JUnit5AppTest
Results :
Tests run: 13, Failures: 0, Errors: 0, Skipped: 3
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.250 s
[INFO] Finished at: 2017-04-29T17:08:56-05:00
[INFO] Final Memory: 11M/309M
[INFO] ------------------------------------------------------------------------
|
打開一個終端窗口 (Mac) 或命令提示 (Windows),導航到您將 HelloJUnit5 應用程序克隆到的目錄,而後輸入此命令:
gradle clean test
|
輸出應相似於:
$ gradle clean test
:clean
:compileJava
:processResources NO-SOURCE
:classes
:compileTestJava
:processTestResources NO-SOURCE
:testClasses
:junitPlatformTest
Download https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-jul/2.6.2/log4j-jul-2.6.2.pom
Download https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-jul/2.6.2/log4j-jul-2.6.2.jar
ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
19:44:36.657 [main] INFO com.makotojava.learn.hellojunit5.JUnit5AppTest - As written, this test will always pass!
19:44:36.667 [main] INFO com.makotojava.learn.hellojunit5.solution.JUnit5AppTest - As written, this test will always pass!
Test run finished after 10145 ms
[ 8 containers found ]
[ 0 containers skipped ]
[ 8 containers started ]
[ 0 containers aborted ]
[ 8 containers successful ]
[ 0 containers failed ]
[ 13 tests found ]
[ 2 tests skipped ]
[ 11 tests started ]
[ 1 tests aborted ]
[ 10 tests successful ]
[ 0 tests failed ]
:test SKIPPED
BUILD SUCCESSFUL
Total time: 18.301 secs
|
如今您已瞭解 JUnit Jupiter,查看了代碼示例,並觀看了視頻(但願您已跟隨視頻進行操做)。很是棒,但沒有什麼比動手編寫代碼更有用了!在第 1 部分的最後一節,您將完成如下任務:
App
類,讓您的單元測試經過檢查。採用真正的測試驅動開發 (TDD) 方式,首先編寫單元測試,運行它們,並會觀察到它們所有失敗了。而後編寫實現,直到單元測試經過,這時您就大功告成了。
注意,JUnit5AppTest
類僅提供了兩個現成的測試方法。首次運行該類時,兩者都是 「綠色」 的。要完成這些練習,您須要添加剩餘的代碼,包括用於告訴 JUnit 運行哪些測試方法的註解。記住,若是沒有正確配備一個類或方法,JUnit 將跳過它。
若是遇到困難,請查閱 com.makotojava.learn.hellojunit5.solution
包來尋找解決方案。
首先從 JUnit5AppTest.java
開始。打開此文件並按照 Javadoc 註解中的指示操做。
提示:使用 Eclipse 中的 Javadoc 視圖讀取測試指令。要打開 Javadoc 視圖,能夠轉到 Window > Show View > Javadoc。您應該看到 Javadoc 視圖。根據您設置工做區的方式,該窗口可能出如今任意多個位置。在個人工做區中,該窗口與圖 3 中的屏幕截圖相似,出如今 IDE 右側的編輯器窗口下方:
編輯器窗口中顯示了具備原始 HTML 標記的 Javadoc 註解,但在 Javadoc 窗口中,已將其格式化,所以更易於閱讀。
若是您像我同樣,您會使用 IDE 執行如下工做:
JUnit 5 提供了一個名爲 JUnitPlatform
的類,它容許您在 Eclipse 中運行 JUnit 5 測試。
要在 Eclipse 中運行測試,須要確保您的計算機上擁有示例應用程序。爲此,最輕鬆的方法是從 GitHub 克隆 HelloJUnit5 應用程序,而後將它導入 Eclipse 中。(由於本教程的視頻展現瞭如何這麼作,因此這裏將跳過細節,僅提供操做步驟。)
確保您克隆了 GitHub 存儲庫,而後將代碼導入 Eclipse 中做爲新的 Maven 項目。
將該項目導入 Eclipse 中後,打開 Project Explorer 視圖並展開 src/main/test
節點,直至看到 JUnit5AppTest
。要以 JUnit 測試的形式運行它,能夠右鍵單擊它,選擇 Run As > JUnit Test。
App
的單一 add()
方法提供的功能很容易理解,並且在設計上很是簡單。我不但願複雜應用程序的業務邏輯阻礙您對 JUnit Jupiter 的學習。
單元測試經過後,您就大功告成了!記住,若是遇到困難,能夠在 com.makotojava.learn.hellojunit5.solution
包中查找解決方案。
在 JUnit 5 教程的前半部分中,我介紹了 JUnit 5 的架構和組件,並詳細介紹了 JUnit Jupiter API。咱們逐個介紹了 JUnit 5 中最經常使用的註解、斷言和前置條件,並且經過一個快速練習演示瞭如何在 Eclipse、Maven 和 Gradle 中運行測試。
在第 2 部分中,您將瞭解 JUnit 5 的一些高級特性:
那麼您接下來會怎麼作?
第 2 部分
瞭解用於參數注入、參數化測試、動態測試和自定義註解的 JUnit Jupiter 擴展
在本教程的第 1 部分中,我介紹了 JUnit 5 的設置說明,以及 JUnit 5 的架構和組件。還介紹瞭如何使用 JUnit Jupiter API 中的新特性,包括註解、斷言和前置條件。
在本部分中,您將熟悉組成全新 JUnit 5 的另外兩個模塊:JUnit Vintage 和 JUnit Jupiter 擴展模型。我將介紹如何使用這些組件實現參數注入、參數化測試、動態測試和自定義註解等。
與第 1 部分中同樣,我將介紹如何使用 Maven 和 Gradle 運行測試。
請注意,本教程的示例基於 JUnit 5, Milestone 5。
假設您熟悉如下軟件的使用:
要跟隨示例進行操做,您應在計算機上安裝 JDK 八、Eclipse、Maven、Gradle(可選)和 Git。若是缺乏其中的任何工具,可以使用下面的連接下載和安裝它們:
升級到新的重要軟件版本始終存在風險,可是在這裏,升級不只是個好主意,並且還很安全。
由於許多組織對 JUnit 4 (甚至對 JUnit 3)進行了大力投資,因此 JUnit 5 的開發團隊建立了 JUnit Vintage 包,其中包含 JUnit Vintage 測試引擎。JUnit Vintage 可確保現有 JUnit 測試能與使用 JUnit Jupiter 建立的新測試一同運行。
JUnit 5 的架構還支持同時運行多個測試引擎:能夠一同運行 JUnit Vintage 測試引擎和任何其餘兼容 JUnit 5 的測試引擎。
如今您已瞭解 JUnit Vintage,可能想知道它的工做原理。圖 1 給出了來自第 1 部分的 JUnit 5 依賴關係圖,展現了 JUnit 5 中各類包之間的關係。
圖 1 中間行中所示的 JUnit Vintage 旨在提供一條通往 JUnit Jupiter 的 「平穩升級路徑」。兩個 JUnit 5 模塊依賴於 JUnit Vintage:
Runner
,容許在 JUnit 4 環境(好比 Eclipse)中執行測試。Rule
。JUnit Vintage 自己由兩個模塊組成:
由於 JUnit Platform 容許多個測試引擎同時運行,因此可以讓您的 JUnit 3 和 JUnit 4 測試與使用 JUnit Jupiter 編寫的測試並列運行。教程後面將介紹如何執行該操做。
在 Eclipse、Maven 和 Gradle 中運行測試以前,咱們花點時間複習一下基本單元測試的概念。咱們將分析在 JUnit 3 和 JUnit 4 中編寫的測試。
使用 JUnit 3 編寫的測試將按原樣在 JUnit Platform 上運行。只需將 junit-vintage
依賴項包含在構建版本中,其餘部分就能直接運行。
在示例應用程序中,您將看到已包含在示例應用程序中的 Maven POM (pom.xml
) 和 Gradle 構建文件 (build.gradle
),因此您可當即運行這些測試。
清單 1 給出了示例應用程序的一個 JUnit 3 測試的部分內容。它位於 com.makotojava.learn.junit3
包中的 src/test/java
樹中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
.
.
public class PersonDaoBeanTest extends TestCase {
private ApplicationContext ctx;
private PersonDaoBean classUnderTest;
@Override
protected void setUp() throws Exception {
ctx = new AnnotationConfigApplicationContext(TestSpringConfiguration.class);
classUnderTest = ctx.getBean(PersonDaoBean.class);
}
@Override
protected void tearDown() throws Exception {
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
if (dataSource instanceof EmbeddedDatabase) {
((EmbeddedDatabase) dataSource).shutdown();
}
}
public void testFindAll() {
assertNotNull(classUnderTest);
List<
Person
> people = classUnderTest.findAll();
assertNotNull(people);
assertFalse(people.isEmpty());
assertEquals(5, people.size());
}
.
.
}
|
JUnit 3 測試用例擴展了 JUnit 3 API 類 TestCase
(第 3 行),每一個測試方法必須以單詞 test
開頭(第 23 行)。
要在 Eclipse 中運行此測試,可右鍵單擊 Package Explorer 視圖中的測試類,選擇 Run As > Junit Test。
教程後面將介紹如何使用 Maven 和 Gradle 運行此測試。
您的 JUnit 4 測試按原樣在 JUnit Platform 上運行。只需將 junit-vintage
依賴項包含在構建版本中,就能直接運行它。
示例應用程序中包含的 Maven POM 和 Gradle 構建文件 (build.gradle
) 中已包含該依賴項,因此您可當即運行這些測試。
清單 2 給出了示例應用程序的一個 JUnit 4 測試的部分內容。它位於 com.makotojava.learn.junit4
包中的 src/test/java
樹中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
.
.
public class PersonDaoBeanTest {
private ApplicationContext ctx;
private PersonDaoBean classUnderTest;
@Before
public void setUp() throws Exception {
ctx = new AnnotationConfigApplicationContext(TestSpringConfiguration.class);
classUnderTest = ctx.getBean(PersonDaoBean.class);
}
@After
public void tearDown() throws Exception {
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
if (dataSource instanceof EmbeddedDatabase) {
((EmbeddedDatabase) dataSource).shutdown();
}
}
@Test
public void findAll() {
assertNotNull(classUnderTest);
List<
Person
> people = classUnderTest.findAll();
assertNotNull(people);
assertFalse(people.isEmpty());
assertEquals(5, people.size());
}
.
.
}
|
JUnit 4 測試用例以單詞 Test
結尾(第 3 行),每一個測試方法使用 @Test
註解(第 23 行)。
要在 Eclipse 中運行此測試,可右鍵單擊 Package Explorer 視圖中的測試類,選擇 Run As > Junit Test。
教程後面將介紹如何使用 Maven 和 Gradle 運行此測試。
junit-jupiter-migration-support
包中包含了用於後向兼容性的一些選定 Rule
,因此若是您對 JUnit 4 規則進行了大力投資也不用擔憂。在 JUnit 5 中,您將使用 JUnit Jupiter 擴展模型實現 JUnit 4 中的各類規則提供的相同行爲。下一節將介紹如何完成該工做。
經過使用 JUnit 擴展模型,如今任何開發人員或工具供應商都能擴展 JUnit 的核心功能。
要想真正認識到 JUnit Jupiter 擴展模型的開創性,須要理解它如何擴展 JUnit 4 的核心功能。若是您已理解這一點,可跳過下一節。
過去,但願擴展 JUnit 4 核心功能的開發人員或工具供應商會使用 Runner
和 @Rule
。
Runner 一般是 BlockJUnit4ClassRunner
的子類,用於提供 JUnit 中沒有直接提供的某種行爲。目前有許多第三方 Runner
,好比用於運行基於 Spring 的單元測試的 SpringJUnit4ClassRunner
,以及用於處理單元測試中 Mockito 對象的MockitoJUnitRunner
。
必須在測試類級別上使用 @RunWith
註解來聲明 Runner
。@RunWith
接受一個參數:Runner
的實現類。由於每一個測試類最多隻能擁有一個 Runner
,因此每一個測試類最多也只能擁有一個擴展點。
爲了解決 Runner
概念的這一內置限制,JUnit 4.7 引入了 @Rule
。一個測試類可聲明多個 @Rule
,這些規則可在測試方法級別和類級別上運行(而 Runner
只能在類級別上運行)。
鑑於 JUnit 4.7 的 @Rule
解決方法很好地處理了大部分狀況,您可能想知道爲何咱們還須要新的 JUnit Jupiter 擴展模型。下節將解釋其中的緣由。
JUnit 5 的一個核心原則是擴展點優於特性。
這意味着儘管 JUnit 能爲工具供應商和開發人員提供各類特性,但 JUnit 5 團隊更喜歡在架構中提供擴展點。這樣第三方(不管是工具供應商、測試編寫者仍是其餘任何人)就能在這些點上編寫各類擴展。根據 JUnit Wiki 的解釋,優先選擇擴展點有 3 個緣由:
接下來我將解釋如何擴展 JUnit Jupiter API,首先從擴展點開始。
一個擴展點對應於 JUnit test 生命週期中一個預約義的點。從 Java™ 語言的角度講,擴展點是您實現並向 JUnit 註冊(激活)的回調接口。所以,擴展點是回調接口,擴展是該接口的實現。
在本教程中,我將把已實現的擴展點回調接口稱爲擴展。
一旦註冊您的擴展,就會將其激活。在測試生命週期中合適的點上,JUnit 將使用回調接口調用它。
表 1 總結了 JUnit Jupiter 擴展模型中的擴展點。
表 1 中列出的擴展點回調接口已在示例應用程序的 JUnit5ExtensionShowcase
類中實現。可在com.makotojava.learn.junit5
包中的 test/src
樹中找到該類。
要建立擴展,只需實現該擴展點的回調接口。假設我想建立一個在每一個測試方法運行以前就運行的擴展。在此狀況下,我只須要實現 BeforeEachCallback
接口:
1
2
3
4
5
6
|
public class MyBeforeEachCallbackExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) throws Exception {
// Implementation goes here
}
}
|
實現擴展點接口後,須要激活它,這樣 JUnit 才能在測試生命週期中合適的點調用它。經過註冊擴展來激活它。
要激活上述擴展,只需使用 @ExtendWith
註解註冊它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@ExtendWith(MyBeforeEachCallbackExtension.class)
public class MyTestClass {
.
.
@Test
public void myTestMethod() {
// Test code here
}
@Test
public void someOtherTestMethod() {
// Test code here
}
.
.
}
|
當 MyTestClass
運行時,在執行每一個 @Test
方法前,會調用 MyBeforeEachCallbackExtension
。
注意,這種註冊擴展的風格是聲明性的。JUnit 還提供了一種自動註冊機制,它使用了 Java 的 ServiceLoader
機制。此處不會詳細介紹該機制,但 JUnit 5 用戶指南的擴展模型部分中提供了大量的有用信息。
假設您想將一個參數傳遞給 @Test
方法。您如何完成該工做?下面咱們就學習一下。
若是所編寫的測試方法在其簽名中包含一個參數,則必須將該參數解析爲一個實際對象,而後 JUnit 才能調用該方法。一種樂觀的場景以下所示:JUnit (1) 尋找一個實現 ParameterResolver
接口的已註冊擴展;(2) 調用它來解析該參數;(3) 而後調用您的測試方法,傳入解析後的參數值。
ParameterResolver
接口包含 2 個方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package org.junit.jupiter.api.extension;
import static org.junit.platform.commons.meta.API.Usage.Experimental;
import java.lang.reflect.Parameter;
import org.junit.platform.commons.meta.API;
@API(Experimental)
public interface ParameterResolver extends Extension {
boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException;
Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException;
}
|
Jupiter 測試引擎須要解析您的測試類中的一個參數時,它首先會調用 supports()
方法,查看該擴展是否能處理這種參數類型。若是 supports()
返回 true
,則 Jupiter 測試引擎調用 resolve()
來獲取正確類型的 Object
,隨後在調用測試方法時會使用該對象。
若是未找到能處理該參數類型的擴展,您會看到一條與下面相似的消息:
1
2
3
4
5
|
org.junit.jupiter.api.extension.ParameterResolutionException:
No ParameterResolver registered for parameter [java.lang.String arg0] in executable
[public void com.makotojava.learn.junit5.PersonDaoBeanTest$WhenDatabaseIsPopulated.findAllByLastName(java.lang.String)].
.
.
|
要建立一個 ParameterResolver
,您只需實現該接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import com.makotojava.learn.junit.Person;
import com.makotojava.learn.junit.PersonGenerator;
public class GeneratedPersonParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return parameterContext.getParameter().getType() == Person.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return PersonGenerator.createPerson();
}
}
|
在這個特定的用例中,若是參數的類型是 Person
(第 14 行),則 supports()
返回 true
。JUnit 須要將參數解析爲 Person
對象時,它調用 resolve()
,後者返回一個新生成的 Person
對象(第 20 行)。
要使用 ParameterResolver
,必須向 JUnit Jupiter 測試引擎註冊它。與前面的演示同樣,可以使用 @ExtendWith
註解完成註冊工做。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@DisplayName("Testing PersonDaoBean")
@ExtendWith(GeneratedPersonParameterResolver.class)
public class PersonDaoBeanTest extends AbstractBaseTest {
.
.
@Test
@DisplayName("Add generated Person should succeed - uses Parameter injection")
public void add(Person person) {
assertNotNull(classUnderTest, "PersonDaoBean reference cannot be null.");
Person personAdded = classUnderTest.add(person);
assertNotNull(personAdded, "Add failed but should have succeeded");
assertNotNull(personAdded.getId());
performPersonAssertions(person.getLastName(), person.getFirstName(), person.getAge(), person.getEyeColor(),
person.getGender(), personAdded);
}
.
.
}
|
PersonDaoBeanTest
類運行時,它將向 Jupiter 測試引擎註冊 GeneratedPersonParameterResolver
。每次須要解析一個參數時,就會調用自定義 ParameterResolver
。
擴展有一個影響範圍 - 類級別或方法級別。
在這個特定的用例中,我選擇在類級別註冊擴展(第 2 行)。在類級別註冊意味着,接受任何參數的任何測試方法都會致使 JUnit 調用 GeneratedPersonParameterResolver
擴展。若是參數類型爲 Person
,則返回一個已生成的 Person
對象並將其傳遞給測試方法(第 8 行)。
要將擴展的範圍縮小到單個方法,可按以下方式註冊擴展:
1
2
3
4
5
6
7
8
9
10
11
|
@Test
@DisplayName("Add generated Person should succeed - uses Parameter injection")
@ExtendWith(GeneratedPersonParameterResolver.class)
|