使用 JUnit 5.7 進行參數化測試:深入瞭解 @EnumSource

使用 JUnit 5.7 進行參數化測試:深入瞭解 @EnumSource    

     參數化測試允許開發人員使用一系列輸入值高效地測試他們的代碼。在 JUnit 測試領域,經驗豐富的用戶長期以來一直在努力解決實施這些測試的複雜問題。但隨着 JUnit 5.7 的發佈,測試參數化進入了一個新時代,爲開發人員提供了一流的支持和增強的功能。讓我們深入探討 JUnit 5.7 爲參數化測試帶來的激動人心的可能性!

image

JUnit 5.7 文檔中的參數化示例 讓我們看看文檔中的一些示例:

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
     assertTrue(StringUtils.isPalindrome(candidate));
}

@ParameterizedTest
@CsvSource({
     "apple,         1",
     "banana,        2",
     "'lemon, lime', 0xF1",
     "strawberry,    700_000"
})


void testWithCsvSource(String fruit, int rank) {
     assertNotNull(fruit);
     assertNotEquals(0, rank);
}

@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
     assertEquals(5, str.length());
     assertTrue(num >=1 && num <=2);
     assertEquals(2, list.size());
}

static Stream<Arguments> stringIntAndListProvider() {
     return Stream.of(
         arguments("apple", 1, Arrays.asList("a", "b")),
         arguments("lemon", 2, Arrays.asList("x", "y"))
     );
}

在使用 @ParameterizedTest 註解的同時,還必須使用所提供的幾個源註解之一,說明從何處獲取參數。參數來源通常被稱爲 "數據提供者"。

@RunWith(JUnit4.class)
@SpringBootTest
public class MyTest {

    @ParameterizedTest
     @ValueSource(ints = {1, 2, 3})
     public void testWithIntValue(int value) {
         // ...
     }

}

在此,我將不對其進行詳細描述:JUnit 用戶指南比我做得更好,但請允許我分享幾點看法:

@ValueSource 只限於提供單個參數值。換句話說,測試方法不能有一個以上的參數,而且可以使用的類型也受到限制。
@CsvSource 在一定程度上解決了傳遞多個參數的問題,它將每個字符串解析爲一條記錄,然後作爲參數逐字段傳遞。對於長字符串和/或大量參數,這很容易造成閱讀困難。此外,可以使用的類型也受到了限制--稍後再詳述。
在註解中聲明實際值的所有來源都被限制爲編譯時常量(Java 註釋的限制,而非 JUnit)。
@MethodSource 和 @ArgumentsSource 提供了(無類型的)n 個元組的流/集合,這些元組將作爲方法參數傳遞。我們支持各種實際類型來表示 n 個元組的序列,但它們都不能保證符合方法的參數列表。這種源需要額外的方法或類,但對從哪裏和如何獲取測試數據沒有限制。

正如您所看到的,可用的源類型從簡單類型(使用簡單,但功能有限)到最終需要更多代碼才能運行的靈活類型不等。

Sidenote --這通常是良好設計的標誌:只需少量代碼就能實現基本功能,而當用於實現要求更高的用例時,增加額外的複雜性是合理的。 @EnumSource 似乎並不符合這種從簡單到靈活的連續性假設。請看這個包含四個參數集的非複雜示例,每個參數集有兩個值。

Note --雖然 @EnumSource 將枚舉的值作爲單個測試方法參數傳遞,但從概念上講,測試是由枚舉字段參數化的,因此對參數數量沒有限制。

    enum Direction {
         UP(0, '^'),
         RIGHT(90, '>'),
         DOWN(180, 'v'),
         LEFT(270, '<');

        private final int degrees;
         private final char ch;

        Direction(int degrees, char ch) {
             this.degrees = degrees;
             this.ch = ch;
         }
     }

    @ParameterizedTest
     @EnumSource
     void direction(Direction dir) {
         assertEquals(0, dir.degrees % 90);
         assertFalse(Character.isWhitespace(dir.ch));
        
         int orientation = player.getOrientation();
         player.turn(dir);
         assertEquals((orientation + dir.degrees) % 360, player.getOrientation());
     }

試想一下:硬編碼的值列表嚴重限制了它的靈活性(不能使用外部數據或生成數據),而聲明枚舉所需的額外代碼量又使它比 @CsvSource 更爲冗長。

但這只是第一印象。我們將看到,當利用 Java 枚舉的真正威力時,它會變得多麼優雅。

附註:本文不涉及驗證生產代碼中的枚舉。當然,無論您選擇何種驗證方式,都必須聲明這些枚舉。本文的重點是何時以及如何以枚舉的形式表達測試數據。


何時使用
在某些情況下,枚舉的性能比其他方法更好:

每次測試多個參數
       當您只需要一個參數時,您可能不想讓 @ValueSource 以外的事情變得複雜。但一旦您需要多個參數,例如輸入和預期結果,您就必須使用 @CsvSource、@MethodSource/@ArgumentsSource 或 @EnumSource。

在某種程度上,枚舉可以讓你 "偷渡 "任意數量的數據字段。

因此,當您將來需要添加更多測試方法參數時,只需在現有的枚舉中添加更多的字段,而無需觸動測試方法簽名。當你在多個測試中重複使用數據提供者時,這一點就變得非常重要。

對於其他數據源,必須使用 ArgumentsAccessors 或 ArgumentsAggregators 才能獲得枚舉開箱即用的靈活性。

類型安全
對於 Java 開發人員來說,這應該是一個大問題。

從 CSV(文件或字面)、@MethodSource 或 @ArgumentsSource 讀取的參數無法在編譯時保證參數數量及其類型與簽名相匹配。

顯然,JUnit 會在運行時發出抱怨,但 IDE 不會提供任何代碼幫助。

和以前一樣,當你在多個測試中重複使用相同的參數時,這種情況就會增加。在將來擴展參數集時,使用類型安全的方法將大有裨益。

自定義類型
對於基於文本的數據源(如從 CSV 讀取數據的數據源)來說,這主要是一個優勢--文本中編碼的值需要轉換爲 Java 類型。

如果要從 CSV 記錄中實例化一個自定義類,可以使用 ArgumentsAggregator 來實現。但是,您的數據聲明仍然不是類型安全的--方法簽名和聲明數據之間的任何不匹配都會在運行時 "聚合 "參數時彈出。更不用說,聲明聚合器類會增加參數化工作所需的更多支持代碼。爲了避免額外的代碼,我們更傾向於使用 @CsvSource 而不是 @EnumSource。

可文檔化
      與其他方法不同,枚舉源爲參數集(枚舉實例)及其包含的所有參數(枚舉字段)都提供了 Java 符號。它們爲以 JavaDoc 這種更自然的形式附加文檔提供了一個直接的位置。

文檔並非不能放置在其他地方,但顧名思義,它將被放置在離文檔更遠的地方,因此更難找到,也更容易過時。

但還有更多枚舉。是類。

許多初級開發人員還沒有意識到 Java 枚舉的真正強大之處。

在其他編程語言中,枚舉只是美化了的常量。但在 Java 中,它們是 Flyweight 設計模式的便捷小實現,具有完整類的(大部分)優點。

爲什麼這是件好事呢?

測試與夾具相關的行爲

     與其他任何類一樣,枚舉可以添加方法。如果枚舉測試參數在不同測試間重複使用,這就變得非常方便--相同的數據,只是測試方式略有不同。爲了在不進行大量複製和粘貼的情況下有效使用參數,這些測試之間還需要共享一些輔助代碼。這不是一個輔助類和幾個靜態方法所能 "解決 "的。附註:請注意,這樣的設計會讓人產生 "功能嫉妒"(Feature Envy)。測試方法--或者更糟糕的是,輔助類方法--必須從枚舉對象中提取數據,才能對這些數據執行操作。雖然這是過程式編程的(唯一)方法,但在面向對象的世界裏,我們可以做得更好。 在枚舉聲明中直接聲明 "輔助 "方法,我們就可以把代碼移到數據所在的地方。或者,用 OOP 術語來說,輔助方法將成爲以枚舉形式實現的測試夾具的 "行爲"。這不僅會使代碼更加習以爲常(在實例上調用合理的方法而不是傳遞數據的靜態方法),而且還能使枚舉參數在測試用例中更容易地重複使用。

繼承
枚舉可以實現帶有(默認)方法的接口。在合理使用的情況下,可以利用這一點在多個數據提供者(多個枚舉)之間共享行爲。我很容易想到的一個例子就是爲正向測試和負向測試分別建立枚舉。如果它們代表了類似的測試夾具,那麼它們就有可能共享某些行爲。

Talk is cheap空談誤國

     讓我們用一個假設的源代碼文件轉換器測試套件來說明這一點,它與執行 Python 2 到 Python 3 轉換的測試套件不太一樣。要想對這樣一個綜合工具的工作有真正的信心,我們最終需要大量的輸入文件,這些文件體現了語言的各個方面,還需要匹配文件來比較轉換結果。除此之外,還需要驗證有問題的輸入會向用戶發出哪些警告/錯誤。 由於需要驗證的樣本數量龐大,這自然適合參數化測試,但由於數據有些複雜,它不太適合任何簡單的 JUnit 參數源。

   enum Conversion {
         CLEAN("imports-correct.2.py", "imports-correct.3.py", Set.of()),
         WARNINGS("problematic.2.py", "problematic.3.py", Set.of(
                 "Using module 'xyz' that is deprecated"
         )),
         SYNTAX_ERROR("syntax-error.py", new RuntimeException("Syntax error on line 17"));
         // Many, many others ...

        @Nonnull
         final String inFile;
         @CheckForNull
         final String expectedOutput;
         @CheckForNull
         final Exception expectedException;
         @Nonnull
         final Set<String> expectedWarnings;

        Conversion(@Nonnull String inFile, @Nonnull String expectedOutput, @NotNull Set<String> expectedWarnings) {
             this(inFile, expectedOutput, null, expectedWarnings);
         }

        Conversion(@Nonnull String inFile, @Nonnull Exception expectedException) {
             this(inFile, null, expectedException, Set.of());
         }

        Conversion(@Nonnull String inFile, String expectedOutput, Exception expectedException, @Nonnull Set<String> expectedWarnings) {
             this.inFile = inFile;
             this.expectedOutput = expectedOutput;
             this.expectedException = expectedException;
             this.expectedWarnings = expectedWarnings;
         }

        public File getV2File() { ... }

        public File getV3File() { ... }
     }

    @ParameterizedTest
     @EnumSource
     void upgrade(Conversion con) {

        try {
             File actual = convert(con.getV2File());
             if (con.expectedException != null) {
                 fail("No exception thrown when one was expected", con.expectedException);
             }
             assertEquals(con.expectedWarnings, getLoggedWarnings());
             new FileAssert(actual).isEqualTo(con.getV3File());
         } catch (Exception ex) {
             assertTypeAndMessageEquals(con.expectedException, ex);
         }
     }

使用枚舉並不會限制數據的複雜程度。正如你所看到的,我們可以在枚舉中定義幾個方便的構造函數,因此聲明新的參數集非常簡潔。這就避免了使用長參數列表的情況,因爲長參數列表中往往充滿了許多 "空 "值(空值、空字符串或集合),讓人不知道 7 號參數--也就是空值之一--究竟代表什麼。請注意,枚舉允許使用複雜類型(Set、RuntimeException),而沒有任何限制或神奇的轉換。傳遞此類數據也是完全類型安全的。實際上,你將有更多的數據樣本需要驗證,因此模板代碼的數量相比之下就不那麼重要了。

此外,還可以看看如何利用相同的枚舉及其輔助方法編寫相關測試:

    @ParameterizedTest
     @EnumSource
     // Upgrading files already upgraded always passes, makes no changes, issues no warnings.
     void upgradeFromV3toV3AlwaysPasses(Conversion con) throws Exception {
         File actual = convert(con.getV3File());
         assertEquals(Set.of(), getLoggedWarnings());
         new FileAssert(actual).isEqualTo(con.getV3File());
     }

    @ParameterizedTest
     @EnumSource
     // Downgrading files created by upgrade procedure is expected to always pass without warnings.
     void downgrade(Conversion con) throws Exception {
         File actual = convert(con.getV3File());
         assertEquals(Set.of(), getLoggedWarnings());
         new FileAssert(actual).isEqualTo(con.getV2File());
     }

    多說幾句 從概念上講,@EnumSource 鼓勵您創建複雜的、機器可讀的單個測試場景描述,模糊了數據提供程序和測試夾具之間的界限。將每個數據集表示爲 Java 符號(枚舉元素)的另一個好處是,它們可以單獨使用,完全脫離數據提供程序/參數化測試。由於它們有一個合理的名稱,而且自成一體(在數據和行爲方面),因此它們有助於形成漂亮和可讀的測試。

@Test
void warnWhenNoEventsReported() throws Exception {
     FixtureXmls.Invalid events = FixtureXmls.Invalid.NO_EVENTS_REPORTED;
    
     // read() is a helper method that is shared by all FixtureXmls
     try (InputStream is = events.read()) {
         EventList el = consume(is);
         assertEquals(Set.of(...), el.getWarnings());
     }
}

現在,@EnumSource 不會成爲你最常用的參數源之一,這是好事,因爲過度使用它不會有任何好處。但在適當的情況下,知道如何使用它們所提供的一切,還是很方便的。



今天先到這兒,希望對雲原生,技術領導力, 企業管理,系統架構設計與評估,團隊管理, 項目管理, 產品管管,團隊建設 有參考作用 , 您可能感興趣的文章:
領導人怎樣帶領好團隊
構建創業公司突擊小團隊
國際化環境下系統架構演化
微服務架構設計
視頻直播平臺的系統架構演化
微服務與Docker介紹
Docker與CI持續集成/CD
互聯網電商購物車架構演變案例
互聯網業務場景下消息隊列架構
互聯網高效研發團隊管理演進之一
消息系統架構設計演進
互聯網電商搜索架構演化之一
企業信息化與軟件工程的迷思
企業項目化管理介紹
軟件項目成功之要素
人際溝通風格介紹一
精益IT組織與分享式領導
學習型組織與企業
企業創新文化與等級觀念
組織目標與個人目標
初創公司人才招聘與管理
人才公司環境與企業文化
企業文化、團隊文化與知識共享
高效能的團隊建設
項目管理溝通計劃
構建高效的研發與自動化運維
某大型電商雲平臺實踐
互聯網數據庫架構設計思路
IT基礎架構規劃方案一(網絡系統規劃)
餐飲行業解決方案之客戶分析流程
餐飲行業解決方案之採購戰略制定與實施流程
餐飲行業解決方案之業務設計流程
供應鏈需求調研CheckList
企業應用之性能實時度量系統演變

如有想了解更多軟件設計與架構, 系統IT,企業信息化, 團隊管理 資訊,請關注我的微信訂閱號:

image

作者:Petter Liu
出處:http://www.cnblogs.com/wintersun/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。 該文章也同時發佈在我的獨立博客中-Petter Liu Blog。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章