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

PetterLiu發表於2024-03-17

使用 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。

相關文章