JUnit 5系列之基礎(chǔ)入門介紹
上周我們剛剛搭建好了 JUnit 5 的環(huán)境,現(xiàn)在我們可以寫測(cè)試了。這節(jié)就讓我們來(lái)寫它幾個(gè)吧!
概述
本文章是這個(gè) JUnit 5 系列的一部分:
(如果不喜歡看文章,你可以戳這里看我的演講,或者看一下最近的 vJUG 講座,或者我在 DevoxxPL 上的 PPT。
本系列文章都基于 Junit 5發(fā)布的先行版 Milestone 2。它可能會(huì)有變化。如果有新的里程碑(milestone)版本發(fā)布,或者試用版正式發(fā)行時(shí),我會(huì)再來(lái)更新這篇文章。
這里要介紹的多數(shù)知識(shí)你都可以在 JUnit 5 用戶指南 中找到(這個(gè)鏈接指向的是先行版 Milestone 2,想看的***版本文檔的話請(qǐng)戳這里),并且指南還有更多的內(nèi)容等待你發(fā)掘。下面的所有代碼都可以在 我的 Github 上找到。
目錄
- 設(shè)計(jì)哲學(xué)
- JUnit 5預(yù)備:
。包可見性
。測(cè)試的生命周期
# @Test
# Before 和 After
# 禁用測(cè)試
# 測(cè)試類的生命周期
。斷言
# 常規(guī)斷言
# 擴(kuò)展斷言
。假言/判定(Assumptions)
。測(cè)試嵌套
。測(cè)試命名
- 回顧
- 分享&關(guān)注
設(shè)計(jì)哲學(xué)
新的架構(gòu)設(shè)計(jì)(這個(gè)我們?nèi)蘸罅?,其關(guān)注點(diǎn)在高擴(kuò)展性。如果后面出現(xiàn)了什么神之測(cè)試技術(shù)(至少對(duì)我們廣大 Java?來(lái)說(shuō)很神的),它們也可能在 JUnit 5 的架構(gòu)下被實(shí)現(xiàn)。
不過(guò)當(dāng)前來(lái)說(shuō),涉及的基礎(chǔ)知識(shí)與 JUnit 4 是非常相似的。JUnit 5 的改動(dòng)并不激進(jìn),相反它的優(yōu)化歷程是小心翼翼,小步迭代的。因此,開發(fā)者應(yīng)該會(huì)對(duì)新的 API 感到非常熟悉。至少我是這樣的,我相信你也不會(huì)感覺陌生:
- class Lifecycle {
- @BeforeAll
- static void initializeExternalResources() {
- System.out.println("Initializing external resources...");
- }
- @BeforeEach
- void initializeMockObjects() {
- System.out.println("Initializing mock objects...");
- }
- @Test
- void someTest() {
- System.out.println("Running some test...");
- assertTrue(true);
- }
- @Test
- void otherTest() {
- assumeTrue(true);
- System.out.println("Running another test...");
- assertNotEquals(1, 42, "Why wouldn't these be the same?");
- }
- @Test
- @Disabled
- void disabledTest() {
- System.exit(1);
- }
- @AfterEach
- void tearDown() {
- System.out.println("Tearing down...");
- }
- @AfterAll
- static void freeExternalResources() {
- System.out.println("Freeing external resources...");
- }
- }
是吧?這里并沒(méi)有很大的改動(dòng)。
JUnit 5 預(yù)備
包可見性
JUnit 5 最明顯的變化應(yīng)該是,不再需要手動(dòng)將測(cè)試類與測(cè)試方法為 public 了。包可見的訪問(wèn)級(jí)別就足夠了。當(dāng)然,私有(private)訪問(wèn)還是不行的。我認(rèn)為這個(gè)變化是合理的,也符合我們對(duì)可見性的一般直覺。
這很好!至少可以少打幾個(gè)字母了。不過(guò),我相信你也不是每次都手打這幾個(gè)字母的,是吧?盡管如此還是很好,少一些關(guān)鍵字,你在看測(cè)試的時(shí)候也少些切換。
測(cè)試的生命周期
@Test
JUnit 中最基本的注解非 @Test 莫屬了。它會(huì)標(biāo)記方法為測(cè)試方法,以便構(gòu)建工具和 IDE 能夠識(shí)別并執(zhí)行它們。
它的 API 和作用并沒(méi)有變化,不過(guò)它不再接受任何參數(shù)了。若要測(cè)試是否拋出異常,你可以通過(guò)新的斷言 API 來(lái)做到;不過(guò)就我所知,目前還沒(méi)有超時(shí)選項(xiàng)timeout的替代品。
與 JUnit 4一樣,JUnit 5 會(huì)為每個(gè)測(cè)試方法創(chuàng)建一個(gè)新的實(shí)例。
Before 和 After
你可能需要執(zhí)行一些代碼來(lái)在測(cè)試執(zhí)行前后完成一些初始化或銷毀的操作。在 JUnit 5 中,有4個(gè)注解你可能會(huì)用于如此工作:
@BeforeAll
只執(zhí)行一次,執(zhí)行時(shí)機(jī)是在所有測(cè)試和 @BeforeEach 注解方法之前。
@BeforeEach
在每個(gè)測(cè)試執(zhí)行之前執(zhí)行。
@AfterEach
在每個(gè)測(cè)試執(zhí)行之后執(zhí)行。
@AfterAll
只執(zhí)行一次,執(zhí)行時(shí)機(jī)是在所有測(cè)試和 @AfterEach 注解方法之后。
因?yàn)榭蚣軙?huì)為每個(gè)測(cè)試創(chuàng)建一個(gè)單獨(dú)的實(shí)例,在 @BeforeAll/@AfterAll 方法執(zhí)行時(shí)尚無(wú)任何測(cè)試實(shí)例誕生。因此,這兩個(gè)方法必須定義為靜態(tài)方法。
注解了同樣一個(gè)注解的不同方法,其執(zhí)行次序是不可預(yù)知的,包括對(duì)繼承來(lái)的方法也適用。這是開發(fā)團(tuán)隊(duì)經(jīng)過(guò)審慎思考后的決定,即把單元測(cè)試與集成測(cè)試的關(guān)注點(diǎn)分開。集成測(cè)試可能需要方法間更緊密的協(xié)作,但一個(gè)單元測(cè)試不應(yīng)該對(duì)其他的單元測(cè)試有所依賴。而對(duì)于集成測(cè)試——也叫場(chǎng)景測(cè)試——的支持,也已在團(tuán)隊(duì)的計(jì)劃中。
除了名字有所不同,這幾個(gè)注解與 JUnit 4 中的注解工作方式完全一樣。無(wú)獨(dú)有偶,跟主流意見一致,我也覺得這個(gè)新的命名不能說(shuō)服我其必要性。這個(gè) issue 下有更多的討論。
禁用測(cè)試
今兒星期五,抬頭一看已經(jīng)4點(diǎn)半,無(wú)心工作的你想回家了?完全理解,在測(cè)試上怒拍一個(gè) @Disabled 注解即可。有良心的話寫個(gè)忽略測(cè)試的理由是極好的,不過(guò)也可以不帶此參數(shù)。
- @Test
- @Disabled("你丫就是存心跑不過(guò)的是不?!")
- void failingTest() {
- assertTrue(false);
- }
測(cè)試類的生命周期
JUnit 團(tuán)隊(duì)發(fā)布的***版原型中,包含了一個(gè)對(duì) 測(cè)試類的生命周期 的描述,有意思的是,這個(gè)特性在 alpha 版本的發(fā)布中未被加入。這個(gè)生命周期模型建議,在被測(cè)類的多個(gè)測(cè)試方法中使用一個(gè)同樣的實(shí)例,因?yàn)檫@樣我們就可以通過(guò)改變對(duì)象的狀態(tài),進(jìn)而實(shí)現(xiàn)在多個(gè)測(cè)試方法中的交互。(我也再說(shuō)一遍,這更像是 場(chǎng)景測(cè)試 要管的事。)
正如我在***版公測(cè)時(shí)所說(shuō),這樣的特性99%的場(chǎng)景下是有害的,只有另外1%的場(chǎng)合下才有真正的用處。我只能說(shuō),還好這個(gè)特性被摒棄了。想想你的單元測(cè)試,如果它們必須靠在方法間維護(hù)狀態(tài)來(lái)工作,這畫面簡(jiǎn)直太美我不敢看?。
斷言
如果說(shuō) @Test、@Before...、@After... 等注解是一個(gè)測(cè)試套件的骨架,那么斷言就是它的心臟。準(zhǔn)備好測(cè)試實(shí)例、執(zhí)行了被測(cè)類的方法以后,斷言能確保你得到了想要的結(jié)果。否則,就說(shuō)明當(dāng)前測(cè)試失敗了。
常規(guī)斷言
一般的斷言,無(wú)非是檢查一個(gè)實(shí)例的屬性(比如,判空與判非空等),或者對(duì)兩個(gè)實(shí)例進(jìn)行比較(比如,檢查兩個(gè)實(shí)例對(duì)象是否相等)等。無(wú)論哪種檢查,斷言方法都可以接受一個(gè)字符串作為***一個(gè)可選參數(shù),它會(huì)在斷言失敗時(shí)提供必要的描述信息。如果提供出錯(cuò)信息的過(guò)程比較復(fù)雜,它也可以被包裝在一個(gè) lambda 表達(dá)式中,這樣,只有到真正失敗的時(shí)候,消息才會(huì)真正被構(gòu)造出來(lái)。
- @Test
- void assertWithBoolean() {
- assertTrue(true);
- assertTrue(this::truism);
- assertFalse(false, () -> "Really " + "expensive " + "message" + ".");
- }
- boolean truism() {
- return true;
- }
- @Test
- void assertWithComparison() {
- List<String> expected = asList("element");
- List<String> actual = new LinkedList<>(expected);
- assertEquals(expected, actual);
- assertEquals(expected, actual, "Should be equal.");
- assertEquals(expected, actual, () -> "Should " + "be " + "equal.");
- assertNotSame(expected, actual, "Obviously not the same instance.");
- }
如你所見,JUnit 5 的 API 并無(wú)太多變化。斷言方法的命名是一樣的,方法同樣接受兩個(gè)參數(shù),分別是一個(gè)期望值與一個(gè)實(shí)際值。
期望值與實(shí)際值的傳入順序非常重要,無(wú)論是對(duì)于理解測(cè)試的內(nèi)容,還是理解失敗時(shí)的錯(cuò)誤信息,但有時(shí)還是很容易弄錯(cuò),這點(diǎn)很坑。不過(guò)仔細(xì)想想,也沒(méi)什么更好的辦法,除非你自己創(chuàng)建一個(gè)新的斷言框架。既然市面上已有對(duì)應(yīng)的產(chǎn)品如 Hamcrest (ugh!) 和AssertJ (yeah!譯者表示:不太清楚這歡呼的梗在哪里)等,再浪費(fèi)有限的時(shí)間去造輪子明顯不值得。畢竟最重要的是保證你的斷言庫(kù)專注于一件事,借鑒已有實(shí)現(xiàn)可以節(jié)省成本。
哦對(duì)了,失敗信息現(xiàn)在是作為***傳入的參數(shù)了。我很喜歡這個(gè)細(xì)節(jié),因?yàn)?,它讓你專注于真正重要之?mdash;—那兩個(gè)需被斷言的值。由于擁抱了 Java 8 的緣故,真值斷言方法現(xiàn)在也接受 supplier 參數(shù)了,又是一個(gè)暖心的小細(xì)節(jié)。
擴(kuò)展斷言
除了那種一般的檢查特定實(shí)例或?qū)傩缘臄嘌酝?,還有一些其他類型的斷言。
這里要講的***個(gè)甚至都不是個(gè)真正的斷言,它做的事就是強(qiáng)行讓測(cè)試失敗,并提供一個(gè)失敗信息。
- @Test
- void failTheTest() {
- fail("epicly");
- }
還有 assertAll 方法,它接受可變數(shù)量的斷言作為參數(shù),并保證它們?nèi)康玫綀?zhí)行,然后再把錯(cuò)誤信息(如果有)一并匯報(bào)出來(lái)。
- @Test
- void assertAllProperties() {
- Address address = new Address("New City", "Some Street", "No");
- assertAll("address",
- () -> assertEquals("Neustadt", address.city),
- () -> assertEquals("Irgendeinestraße", address.street),
- () -> assertEquals("Nr", address.number)
- );
- }
- org.opentest4j.MultipleFailuresError: address (3 failures)
- expected: <Neustadt> but was: <New City>
- expected: <Irgendeinestraße> but was: <Some Street>
- expected: <Nr> but was: <No>
這個(gè)特性在檢查對(duì)象的多個(gè)屬性值時(shí)非常有用。按照一般的做法,測(cè)試在***個(gè)斷言失敗時(shí)就會(huì)掛掉了,此時(shí)只有***個(gè)出錯(cuò)的地方得到提示,而你無(wú)法得知其他值的斷言是否成功,只好再跑一遍測(cè)試。
***,我們終于有了 assertThrows 和 expectThrows 方法。兩者均會(huì)在被測(cè)方法未拋出預(yù)期異常時(shí)失敗。而后者還會(huì)返回拋出的異常實(shí)例,以用于后續(xù)的驗(yàn)證,比如,斷言異常信息包含正確的信息等。
- @Test
- void assertExceptions() {
- assertThrows(Exception.class, this::throwing);
- Exception exception = expectThrows(Exception.class, this::throwing);
- assertEquals("Because I can!", exception.getMessage());
- }
假言/判定(Assumptions)
假言/判定允許你僅在特定條件滿足時(shí)才運(yùn)行測(cè)試。這個(gè)特性能夠減少測(cè)試組件的運(yùn)行時(shí)間和代碼重復(fù),特別是在假言都不滿足的情況下。
- @Test
- void exitIfFalseIsTrue() {
- assumeTrue(false);
- System.exit(1);
- }
- @Test
- void exitIfTrueIsFalse() {
- assumeFalse(this::truism);
- System.exit(1);
- }
- private boolean truism() {
- return true;
- }
- @Test
- void exitIfNullEqualsString() {
- assumingThat(
- "null".equals(null),
- () -> System.exit(1)
- );
- }
假言/判定適用于兩種情形,要么是你希望在某些條件不滿足時(shí)中止測(cè)試,要么是你希望僅當(dāng)某個(gè)條件滿足時(shí)才執(zhí)行(部分)測(cè)試。主要的區(qū)別是,被中止的測(cè)試是以被禁用(disabled)的形式被報(bào)告,此時(shí)沒(méi)有測(cè)試任何內(nèi)容,因?yàn)闂l件得不到滿足。
測(cè)試嵌套
在 JUnit 5 中,嵌套測(cè)試幾乎不費(fèi)吹灰之力。你只需要在嵌套的類上添加 @Nested 注解,類中的所有方法即會(huì)被引擎執(zhí)行:
- package org.codefx.demo.junit5;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Nested;
- import org.junit.jupiter.api.Test;
- import static org.junit.jupiter.api.Assertions.assertEquals;
- import static org.junit.jupiter.api.Assertions.assertTrue;
- class Nest {
- int count = Integer.MIN_VALUE;
- @BeforeEach
- void setCountToZero() {
- count = 0;
- }
- @Test
- void countIsZero() {
- assertEquals(0, count);
- }
- @Nested
- class CountGreaterZero {
- @BeforeEach
- void increaseCount() {
- count++;
- }
- @Test
- void countIsGreaterZero() {
- assertTrue(count > 0);
- }
- @Nested
- class CountMuchGreaterZero {
- @BeforeEach
- void increaseCount() {
- count += Integer.MAX_VALUE / 2;
- }
- @Test
- void countIsLarge() {
- assertTrue(count > Integer.MAX_VALUE / 2);
- }
- }
- }
- }
如你所見,嵌套類中的 @BeforeEach(及 @AfterEach )注解也工作良好。不過(guò),構(gòu)造順序似乎還未被寫入文檔,它們的初始化次序是從外向內(nèi)的。這也讓你能疊加式地為內(nèi)部類準(zhǔn)備測(cè)試數(shù)據(jù)。
如果嵌套的內(nèi)部測(cè)試想要存取外部測(cè)試類的字段,那么嵌套類本身不應(yīng)該是靜態(tài)的。但這樣一來(lái)也就禁止了靜態(tài)方法的使用,因而這種場(chǎng)景下@BeforeAll 和 @AfterAll 方法也就無(wú)法使用了(還是說(shuō)終有他法實(shí)現(xiàn)?)
你可能有疑惑,嵌套的內(nèi)部測(cè)試類有什么用。個(gè)人而言,我用內(nèi)部類來(lái)漸進(jìn)測(cè)試接口,其他人則多用于保持測(cè)試類短小專注。后者同時(shí)也有一個(gè)經(jīng)典的例子來(lái)說(shuō)明,例子由 JUnit 團(tuán)隊(duì)提供,它測(cè)試了一個(gè)棧:
- class TestingAStack {
- Stack<Object> stack;
- boolean isRun = false;
- @Test
- void isInstantiatedWithNew() {
- new Stack<Object>();
- }
- @Nested
- class WhenNew {
- @BeforeEach
- void init() {
- stack = new Stack<Object>();
- }
- // some tests on 'stack', which is empty
- @Nested
- class AfterPushing {
- String anElement = "an element";
- @BeforeEach
- void init() {
- stack.push(anElement);
- }
- // some tests on 'stack', which has one element...
- }
- }
- }
在上面的例子中,棧的狀態(tài)改變會(huì)反映到內(nèi)層的測(cè)試類中,其中內(nèi)部類又基于自身的場(chǎng)景執(zhí)行了一些測(cè)試。
測(cè)試命名
JUnit 5 提供了一個(gè)注解 @DisplayName,它用以為開發(fā)者提供更可讀的測(cè)試類和測(cè)試方法信息。
上面的 stack 測(cè)試?yán)蛹由显撟⒔庖院缶妥兂蛇@樣:
- @DisplayName("A stack")
- class TestingAStack {
- @Test
- @DisplayName("is instantiated with new Stack()")
- void isInstantiatedWithNew() { /*...*/ }
- @Nested
- @DisplayName("when new")
- class WhenNew {
- @Test
- @DisplayName("is empty")
- void isEmpty() { /*...*/ }
- @Test
- @DisplayName("throws EmptyStackException when popped")
- void throwsExceptionWhenPopped() { /*...*/ }
- @Test
- @DisplayName("throws EmptyStackException when peeked")
- void throwsExceptionWhenPeeked() { /*...*/ }
- @Nested
- @DisplayName("after pushing an element")
- class AfterPushing {
- @Test
- @DisplayName("it is no longer empty")
- void isEmpty() { /*...*/ }
- @Test
- @DisplayName("returns the element when popped and is empty")
- void returnElementWhenPopped() { /*...*/ }
- @Test
- @DisplayName(
- "returns the element when peeked but remains not empty")
- void returnElementWhenPeeked(){ /*...*/ }
- }
- }
- }
這是一份TDDer 看了會(huì)感動(dòng),BDDer 看了會(huì)流淚的測(cè)試結(jié)果輸出。
回顧
差不多就這些了,恭喜你終于讀完了。我們匆匆過(guò)完了 JUnit 5 的基本特性,現(xiàn)在,你應(yīng)該了解了所有寫測(cè)試的必備知識(shí)了:包括如何為方法添加生命周期注解(@[Before|After][All|Each]、如何注解測(cè)試方法本身(@Test)、如何嵌套測(cè)試(@Nested)、如何給測(cè)試一個(gè)好信息(@DisplayName),你也應(yīng)該能了解斷言和假言判定是如何工作的了(基本上與前版無(wú)異)。
不過(guò)這可還沒(méi)完!我們還沒(méi)聊到 測(cè)試方法的條件執(zhí)行,沒(méi)聊到非??岬?參數(shù)注入 ,以及 JUnit 5 的擴(kuò)展機(jī)制 和 架構(gòu)體系 呢。放心,這真的是***了,這些話題我們會(huì)一個(gè)月后再聊,現(xiàn)在你可以先休息一下啦。
敬請(qǐng)期待下集!