「譯」JUnit 5 系列:基礎入門

原文地址:http://blog.codefx.org/libraries/junit-5-basics/
原文日期:25, Feb, 2016
譯文首發: Linesh 的博客:JUnit 5 系列:基礎入門
個人 Github:http://github.com/linesh-simplicityhtml

上週咱們剛剛搭建好了 JUnit 5 的環境,如今咱們能夠寫測試了。這節就讓咱們來寫它幾個吧!java

概述

本文章是這個 JUnit 5 系列的一部分:git

(若是不喜歡看文章,你能夠戳這裏看個人演講,或者看一下最近的 vJUG 講座,或者我在 DevoxxPL 上的 PPT

本系列文章都基於 Junit 5發佈的先行版 Milestone 2。它可能會有變化。若是有新的里程碑(milestone)版本發佈,或者試用版正式發行時,我會再來更新這篇文章。

這裏要介紹的多數知識你均可以在 JUnit 5 用戶指南 中找到(這個連接指向的是先行版 Milestone 2,想看的最新版本文檔的話請戳這裏),而且指南還有更多的內容等待你發掘。下面的全部代碼均可以在 個人 Github 上找到。

目錄

  • 設計哲學

  • JUnit 5預備:

    • 包可見性

    • 測試的生命週期

      • @Test

      • Before 和 After

      • 禁用測試

      • 測試類的生命週期

    • 斷言

      • 常規斷言

      • 擴展斷言

    • 假言/斷定(Assumptions)

    • 測試嵌套

    • 測試命名

  • 回顧

  • 分享&關注

設計哲學

新的架構設計(這個咱們往後聊),其關注點在高擴展性。若是後面出現了什麼神之測試技術(至少對咱們廣大 Java?來講很神的),它們也可能在 JUnit 5 的架構下被實現。

不過當前來講,涉及的基礎知識與 JUnit 4 是很是類似的。JUnit 5 的改動並不激進,相反它的優化歷程是當心翼翼,小步迭代的。所以,開發者應該會對新的 API 感到很是熟悉。至少我是這樣的,我相信你也不會感受陌生:

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...");
    }
 
}

是吧?這裏並無很大的改動。

JUnit 5 預備

包可見性

JUnit 5 最明顯的變化應該是,再也不須要手動將測試類與測試方法爲 public 了。包可見的訪問級別就足夠了。固然,私有(private)訪問仍是不行的。我認爲這個變化是合理的,也符合咱們對可見性的通常直覺。

這很好!至少能夠少打幾個字母了。不過,我相信你也不是每次都手打這幾個字母的,是吧?儘管如此仍是很好,少一些關鍵字,你在看測試的時候也少些切換。

測試的生命週期

@Test

JUnit 中最基本的註解非 @Test 莫屬了。它會標記方法爲測試方法,以便構建工具和 IDE 可以識別並執行它們。

它的 API 和做用並無變化,不過它再也不接受任何參數了。若要測試是否拋出異常,你能夠經過新的斷言 API 來作到;不過就我所知,目前尚未超時選項timeout的替代品。

與 JUnit 4同樣,JUnit 5 會爲每一個測試方法建立一個新的實例。

Before 和 After

你可能須要執行一些代碼來在測試執行先後完成一些初始化或銷燬的操做。在 JUnit 5 中,有4個註解你可能會用於如此工做:

@BeforeAll
只執行一次,執行時機是在全部測試和 @BeforeEach 註解方法以前。

@BeforeEach
在每一個測試執行以前執行。

@AfterEach
在每一個測試執行以後執行。

@AfterAll
只執行一次,執行時機是在全部測試和 @AfterEach 註解方法以後。

由於框架會爲每一個測試建立一個單獨的實例,在 @BeforeAll/@AfterAll 方法執行時尚無任何測試實例誕生。所以,這兩個方法必須定義爲靜態方法。

註解了一樣一個註解的不一樣方法,其執行次序是不可預知的,包括對繼承來的方法也適用。這是開發團隊通過審慎思考後的決定,即把單元測試與集成測試的關注點分開。集成測試可能須要方法間更緊密的協做,但一個單元測試不該該對其餘的單元測試有所依賴。而對於集成測試——也叫場景測試——的支持,也已在團隊的計劃中

除了名字有所不一樣,這幾個註解與 JUnit 4 中的註解工做方式徹底同樣。無獨有偶跟主流意見一致,我也以爲這個新的命名不能說服我其必要性。這個 issue 下有更多的討論。

禁用測試

今兒星期五,擡頭一看已經4點半,無意工做的你想回家了?徹底理解,在測試上怒拍一個 @Disabled 註解便可。有良心的話寫個忽略測試的理由是極好的,不過也能夠不帶此參數。

@Test 
@Disabled("你丫就是存心跑不過的是不?!")
void failingTest() {
    assertTrue(false);
}

測試類的生命週期

JUnit 團隊發佈的初版原型中,包含了一個對 測試類的生命週期 的描述,有意思的是,這個特性在 alpha 版本的發佈中未被加入。這個生命週期模型建議,在被測類的多個測試方法中使用一個一樣的實例,由於這樣咱們就能夠經過改變對象的狀態,進而實如今多個測試方法中的交互。(我也再說一遍,這更像是 場景測試 要管的事。)

正如我在初版公測時所說,這樣的特性99%的場景下是有害的,只有另外1%的場合下才有真正的用處。我只能說,還好這個特性被摒棄了。想一想你的單元測試,若是它們必須靠在方法間維護狀態來工做,這畫面簡直太美我不敢看?。

斷言

若是說 @Test@Before...@After... 等註解是一個測試套件的骨架,那麼斷言就是它的心臟。準備好測試實例、執行了被測類的方法之後,斷言能確保你獲得了想要的結果。不然,就說明當前測試失敗了。

常規斷言

通常的斷言,無非是檢查一個實例的屬性(好比,判空與判非空等),或者對兩個實例進行比較(好比,檢查兩個實例對象是否相等)等。不管哪一種檢查,斷言方法均可以接受一個字符串做爲最後一個可選參數,它會在斷言失敗時提供必要的描述信息。若是提供出錯信息的過程比較複雜,它也能夠被包裝在一個 lambda 表達式中,這樣,只有到真正失敗的時候,消息纔會真正被構造出來。

@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 並沒有太多變化。斷言方法的命名是同樣的,方法一樣接受兩個參數,分別是一個指望值與一個實際值。

指望值與實際值的傳入順序很是重要,不管是對於理解測試的內容,仍是理解失敗時的錯誤信息,但有時仍是很容易弄錯,這點很坑。不過仔細想一想,也沒什麼更好的辦法,除非你本身建立一個新的斷言框架。既然市面上已有對應的產品如 Hamcrest (ugh!) 和 AssertJ (yeah!譯者表示:不太清楚這歡呼的梗在哪裏)等,再浪費有限的時間去造輪子明顯不值得。畢竟最重要的是保證你的斷言庫專一於一件事,借鑑已有實現能夠節省成本。

哦對了,失敗信息如今是做爲最後傳入的參數了。我很喜歡這個細節,由於,它讓你專一於真正重要之事——那兩個需被斷言的值。因爲擁抱了 Java 8 的緣故,真值斷言方法如今也接受 supplier 參數了,又是一個暖心的小細節。

擴展斷言

除了那種通常的檢查特定實例或屬性的斷言外,還有一些其餘類型的斷言。

這裏要講的第一個甚至都不是個真正的斷言,它作的事就是強行讓測試失敗,並提供一個失敗信息。

@Test
void failTheTest() {
    fail("epicly");
}

還有 assertAll 方法,它接受可變數量的斷言做爲參數,並保證它們所有獲得執行,而後再把錯誤信息(若是有)一併彙報出來。

@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>

這個特性在檢查對象的多個屬性值時很是有用。按照通常的作法,測試在第一個斷言失敗時就會掛掉了,此時只有第一個出錯的地方獲得提示,而你沒法得知其餘值的斷言是否成功,只好再跑一遍測試。

最後,咱們終於有了 assertThrowsexpectThrows 方法。二者均會在被測方法未拋出預期異常時失敗。然後者還會返回拋出的異常實例,以用於後續的驗證,好比,斷言異常信息包含正確的信息等。

@Test
void assertExceptions() {
    assertThrows(Exception.class, this::throwing);
 
    Exception exception = expectThrows(Exception.class, this::throwing);
    assertEquals("Because I can!", exception.getMessage());
}

假言/斷定(Assumptions)


假言/斷定容許你僅在特定條件知足時才運行測試。這個特性可以減小測試組件的運行時間和代碼重複,特別是在假言都不知足的狀況下。

@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)
    );
}

假言/斷定適用於兩種情形,要麼是你但願在某些條件不知足時停止測試,要麼是你但願僅當某個條件知足時才執行(部分)測試。主要的區別是,被停止的測試是以被禁用(disabled)的形式被報告,此時沒有測試任何內容,由於條件得不到知足。

測試嵌套

在 JUnit 5 中,嵌套測試幾乎不費吹灰之力。你只須要在嵌套的類上添加 @Nested 註解,類中的全部方法即會被引擎執行:

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 )註解也工做良好。不過,構造順序彷佛還未被寫入文檔,它們的初始化次序是從外向內的。這也讓你能疊加式地爲內部類準備測試數據。

若是嵌套的內部測試想要存取外部測試類的字段,那麼嵌套類自己不該該是靜態的。但這樣一來也就禁止了靜態方法的使用,於是這種場景下@BeforeAll@AfterAll 方法也就沒法使用了(仍是說終有他法實現?

你可能有疑惑,嵌套的內部測試類有什麼用。我的而言,我用內部類來漸進測試接口,其餘人則多用於保持測試類短小專一。後者同時也有一個經典的例子來講明,例子由 JUnit 團隊提供,它測試了一個棧:

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...
 
        }
    }
}

在上面的例子中,棧的狀態改變會反映到內層的測試類中,其中內部類又基於自身的場景執行了一些測試。

測試命名

JUnit 5 提供了一個註解 @DisplayName,它用覺得開發者提供更可讀的測試類和測試方法信息。

上面的 stack 測試例子加上該註解之後就變成這樣:

@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 看了會感動,BDDer 看了會流淚的測試結果輸出。

回顧

差很少就這些了,恭喜你終於讀完了。咱們匆匆過完了 JUnit 5 的基本特性,如今,你應該瞭解了全部寫測試的必備知識了:包括如何爲方法添加生命週期註解(@[Before|After][All|Each]、如何註解測試方法自己(@Test)、如何嵌套測試(@Nested)、如何給測試一個好信息(@DisplayName),你也應該能瞭解斷言和假言斷定是如何工做的了(基本上與前版無異)。

不過這可還沒完!咱們還沒聊到 測試方法的條件執行,沒聊到很是酷的 參數注入 ,以及 JUnit 5 的擴展機制架構體系 呢。放心,這真的是最後了,這些話題咱們會一個月後再聊,如今你能夠先休息一下啦。

敬請期待下集!

相關文章
相關標籤/搜索