JUnit 5 參數(shù)化測試


JUnit 5參數(shù)化測試
目錄
設(shè)置
我們的第一個參數(shù)化測試
參數(shù)來源
@ValueSource
@NullSource & @EmptySource
@MethodSource
@CsvSource
@CsvFileSource
@EnumSource
@ArgumentsSource
參數(shù)轉(zhuǎn)換
參數(shù)聚合
獎勵
總結(jié)
如果您正在閱讀這篇文章,說明您已經(jīng)熟悉了JUnit。
讓我為您概括一下JUnit——在軟件開發(fā)中,我們開發(fā)人員編寫的代碼可能是設(shè)計一個人的個人資料這樣簡單,也可能是在銀行系統(tǒng)中進行付款這樣復(fù)雜。在開發(fā)這些功能時,我們傾向于編寫單元測試。顧名思義,單元測試的主要目的是確保代碼的小、單獨部分按預(yù)期功能工作。如果單元測試執(zhí)行失敗,這意味著該功能無法按預(yù)期工作。編寫單元測試的一種工具是JUnit。這些單元測試程序很小,但是非常強大,并且可以快速執(zhí)行。如果您想了解更多關(guān)于JUnit 5(也稱為JUnit Jupiter)的信息,請查看這篇JUnit5的文章。
現(xiàn)在我們已經(jīng)了解了JUnit,接下來讓我們聚焦于JUnit 5中的參數(shù)化測試。參數(shù)化測試可以解決在為任何新/舊功能開發(fā)測試框架時遇到的最常見問題。
編寫針對每個可能輸入的測試用例變得更加容易。
單個測試用例可以接受多個輸入來測試源代碼,有助于減少代碼重復(fù)。
?通過使用多個輸入運行單個測試用例,我們可以確信已涵蓋所有可能的場景,并維護更好的代碼覆蓋率。
開發(fā)團隊通過利用方法和類來創(chuàng)建可重用且松散耦合的源代碼。傳遞給代碼的參數(shù)會影響其功能。例如,計算器類中的sum方法可以處理整數(shù)和浮點數(shù)值。JUnit 5引入了執(zhí)行參數(shù)化測試的能力,可以使用單個測試用例測試源代碼,該測試用例可以接受不同的輸入。這樣可以更有效地進行測試,因為在舊版本的JUnit中,必須為每種輸入類型創(chuàng)建單獨的測試用例,從而導(dǎo)致大量的代碼重復(fù)。
? 示例代碼
本文附帶有在 GitHub上?的一個可工作的示例代碼。
設(shè)置
就像瘋狂泰坦滅霸喜歡訪問力量一樣,您可以使用以下Maven依賴項來訪問JUnit5中參數(shù)化測試的力量:
<dependency>
????<groupId>org.junit.jupiter</groupId>
????<artifactId>junit-jupiter-params</artifactId>
????<version>5.9.2</version>
????<scope>test</scope>
</dependency>
讓我們來寫些代碼,好嗎?
我們的第一個參數(shù)化測試
?現(xiàn)在,我想向您介紹一個新的注解 @ParameterizedTest。顧名思義,它告訴JUnit引擎使用不同的輸入值運行此測試。
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
public class ValueSourceTest {
????@ParameterizedTest
????@ValueSource(ints = { 2, 4 })
????void checkEvenNumber(int number) {
????????assertEquals(0, number % 2,
???????? "Supplied number is not an even number");
????}
}
在上面的示例中,注解@ValueSource為?checkEvenNumber() 方法提供了多個輸入。假設(shè)我們使用JUnit4編寫相同的代碼,即使它們的結(jié)果(斷言)完全相同,我們也必須編寫2個測試用例來覆蓋輸入2和4。
當(dāng)我們執(zhí)行 ValueSourceTest 時,我們會看到什么:
ValueSourceTest
|_ checkEvenNumber
|_ [1] 2
|_ [2] 4
這意味著?checkEvenNumber() 方法將使用2個輸入值執(zhí)行。
在下一節(jié)中,讓我們學(xué)習(xí)一下JUnit5框架提供的各種參數(shù)來源。
參數(shù)來源
JUnit5提供了許多參數(shù)來源注釋。下面的章節(jié)將簡要概述其中一些注釋并提供示例。
@ValueSource
?@ValueSource是一個簡單的參數(shù)源,可以接受單個字面值數(shù)組。@ValueSource支持的字面值類型有short、byte、int、long、float、double、char、boolean、String和Class。
@ParameterizedTest
@ValueSource(strings = { "a1", "b2" })
void checkAlphanumeric(String word) {
????assertTrue(StringUtils.isAlphanumeric(word),
???????????? "Supplied word is not alpha-numeric");
}
@NullSource & @EmptySource
假設(shè)我們需要驗證用戶是否已經(jīng)提供了所有必填字段(例如在登錄函數(shù)中需要提供用戶名和密碼)。我們使用注解來檢查提供的字段是否為 null,空字符串或空格。
在單元測試中使用 @NullSource 和 @EmptySource 可以幫助我們提供帶有 null、空字符串和空格的數(shù)據(jù)源,并驗證源代碼的行為。
@ParameterizedTest
@NullSource
void checkNull(String value) {
????assertEquals(null, value);
}
@ParameterizedTest
@EmptySource
void checkEmpty(String value) {
????assertEquals("", value);
}
我們還可以使用 @NullAndEmptySource 注解來組合傳遞 null 和空輸入。
@ParameterizedTest
@NullAndEmptySource
void checkNullAndEmpty(String value) {
????assertTrue(value == null || value.isEmpty());
}
另一個傳遞 null、空字符串和空格輸入值的技巧是結(jié)合使用 @NullAndEmptySource 注解,以覆蓋所有可能的負面情況。該注解允許我們從一個或多個測試類的工廠方法中加載輸入,并生成一個參數(shù)流。
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", " " })
void checkNullEmptyAndBlank(String value) {
????assertTrue(value == null || value.isBlank());
}
@MethodSource
該注解允許我們從一個或多個測試類的工廠方法中加載輸入,并生成一個參數(shù)流。
顯式方法源 - 測試將嘗試加載提供的方法。
// Note: The test will try to load the supplied method
@ParameterizedTest
@MethodSource("checkExplicitMethodSourceArgs")
void checkExplicitMethodSource(String word) {
assertTrue(StringUtils.isAlphanumeric(word),
"Supplied word is not alpha-numeric");
}
static Stream<String> checkExplicitMethodSourceArgs() {
return Stream.of("a1",
"b2");
}
隱式方法源 - 測試將搜索與測試類匹配的源方法。
// Note: The test will search for the source method
// that matches the test-case method name
@ParameterizedTest
@MethodSource
void checkImplicitMethodSource(String word) {
????assertTrue(StringUtils.isAlphanumeric(word),
"Supplied word is not alpha-numeric");
}
static Stream<String> checkImplicitMethodSource() {
return Stream.of("a1",
"b2");
}
多參數(shù)方法源 - 我們必須將輸入作為參數(shù)流傳遞。測試將按照索引順序加載參數(shù)。
// Note: The test will automatically map arguments based on the index
@ParameterizedTest
@MethodSource
void checkMultiArgumentsMethodSource(int number, String expected) {
????assertEquals(StringUtils.equals(expected, "even") ? 0 : 1, number % 2);
}
static Stream<Arguments> checkMultiArgumentsMethodSource() {
????return Stream.of(Arguments.of(2, "even"),
???? Arguments.of(3, "odd"));
}
外部方法源 - 測試將嘗試加載外部方法。
// Note: The test will try to load the external method
@ParameterizedTest
@MethodSource(
"source.method.ExternalMethodSource#checkExternalMethodSourceArgs")
void checkExternalMethodSource(String word) {
????assertTrue(StringUtils.isAlphanumeric(word),
"Supplied word is not alpha-numeric");
}
package source.method;
import java.util.stream.Stream;
public class ExternalMethodSource {
????static Stream<String> checkExternalMethodSourceArgs() {
????????return Stream.of("a1",
???????? "b2");
????}
}
@CsvSource
該注解允許我們將參數(shù)列表作為逗號分隔的值(即 CSV 字符串字面量)傳遞,每個 CSV 記錄都會導(dǎo)致執(zhí)行一次參數(shù)化測試。它還支持使用 useHeadersInDisplayName屬性跳過 CSV 標(biāo)頭。
@ParameterizedTest
@CsvSource({ "2, even",
"3, odd"})
void checkCsvSource(int number, String expected) {
????assertEquals(StringUtils.equals(expected, "even")
???? ? 0 : 1, number % 2);
}
@CsvFileSource
該注解允許我們使用類路徑或本地文件系統(tǒng)中的逗號分隔值(CSV)文件。與 @CsvSource 類似,每個 CSV 記錄都會導(dǎo)致執(zhí)行一次參數(shù)化測試。它還支持各種其他屬性 -numLinesToSkip、useHeadersInDisplayName、lineSeparator、delimiterString等。
示例 1: 基本實現(xiàn)
@ParameterizedTest
@CsvFileSource(
files = "src/test/resources/csv-file-source.csv",
numLinesToSkip = 1)
void checkCsvFileSource(int number, String expected) {
????assertEquals(StringUtils.equals(expected, "even")
???????????????? ? 0 : 1, number % 2);
}
src/test/resources/csv-file-source.csv
NUMBER, ODD_EVEN
2, even
3, odd
示例2:使用屬性
@ParameterizedTest
@CsvFileSource(
????files = "src/test/resources/csv-file-source_attributes.csv",
????delimiterString = "|",
????lineSeparator = "||",
????numLinesToSkip = 1)
void checkCsvFileSourceAttributes(int number, String expected) {
????assertEquals(StringUtils.equals(expected, "even")
? 0 : 1, number % 2);
}
src/test/resources/csv-file-source_attributes.csv
|| NUMBER | ODD_EVEN ||
|| 2 ???? | even ||
|| 3 ???? | odd???? ||
@EnumSource
該注解提供了一種方便的方法來使用枚舉常量作為測試用例參數(shù)。支持的屬性包括:
value - 枚舉類類型,例如 ChronoUnit.class
package java.time.temporal;
public enum ChronoUnit implements TemporalUnit {
????SECONDS("Seconds", Duration.ofSeconds(1)),
????MINUTES("Minutes", Duration.ofSeconds(60)),
HOURS("Hours", Duration.ofSeconds(3600)),
????DAYS("Days", Duration.ofSeconds(86400)),
????//12 other units
}
?ChronoUnit 是一個包含標(biāo)準(zhǔn)日期周期單位的枚舉類型。
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void checkEnumSourceValue(ChronoUnit unit) {
assertNotNull(unit);
}
在此示例中,@EnumSource 將傳遞所有16個 ChronoUnit 枚舉值作為參數(shù)。
names - 枚舉常量的名稱或選擇名稱的正則表達式,例如 DAYS 或 ^.*DAYS$
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void checkEnumSourceNames(ChronoUnit unit) {
????assertNotNull(unit);
}
@ArgumentsSource
該注解提供了一個自定義的可重用ArgumentsProvider。ArgumentsProvider的實現(xiàn)必須是外部類或靜態(tài)嵌套類。
外部參數(shù)提供程序
public class ArgumentsSourceTest {
????@ParameterizedTest
????@ArgumentsSource(ExternalArgumentsProvider.class)
????void checkExternalArgumentsSource(int number, String expected) {
????????assertEquals(StringUtils.equals(expected, "even")
????????????????????? 0 : 1, number % 2,
????????????????????"Supplied number " + number +
????????????????????" is not an " + expected + " number");
????}
}
public class ExternalArgumentsProvider implements ArgumentsProvider {
????@Override
????public Stream<? extends Arguments> provideArguments(
????????ExtensionContext context) throws Exception {
????????return Stream.of(Arguments.of(2, "even"),
???????????? Arguments.of(3, "odd"));
????}
}
靜態(tài)嵌套參數(shù)提供程序
public class ArgumentsSourceTest {
????@ParameterizedTest
????@ArgumentsSource(NestedArgumentsProvider.class)
????void checkNestedArgumentsSource(int number, String expected) {
????????assertEquals(StringUtils.equals(expected, "even")
? 0 : 1, number % 2,
???????????? ????"Supplied number " + number +
????????????????????" is not an " + expected + " number");
????}
????static class NestedArgumentsProvider implements ArgumentsProvider {
????????@Override
????????public Stream<? extends Arguments> provideArguments(
????????????ExtensionContext context) throws Exception {
????????????return Stream.of(Arguments.of(2, "even"),
???? Arguments.of(3, "odd"));
????????}
????}
}
參數(shù)轉(zhuǎn)換
首先,想象一下如果沒有參數(shù)轉(zhuǎn)換,我們將不得不自己處理參數(shù)數(shù)據(jù)類型的問題。?
源方法: Calculator?類
public int sum(int a, int b) {
????return a + b;
}
測試用例:
@ParameterizedTest
@CsvSource({ "10, 5, 15" })
void calculateSum(String num1, String num2, String expected) {
????int actual = calculator.sum(Integer.parseInt(num1),
????????????????????????????????Integer.parseInt(num2));
????assertEquals(Integer.parseInt(expected), actual);
}
如果我們有String參數(shù),而我們正在測試的源方法接受Integers,則在調(diào)用源方法之前,我們需要負責(zé)進行此轉(zhuǎn)換。
JUnit5 提供了不同的參數(shù)轉(zhuǎn)換方式
擴展原始類型轉(zhuǎn)換
@ParameterizedTest
@ValueSource(ints = { 2, 4 })
void checkWideningArgumentConversion(long number) {
????assertEquals(0, number % 2);
}
使用?@ValueSource(ints = { 1, 2, 3 }) 進行參數(shù)化測試時,可以聲明接受 int、long、float 或 double 類型的參數(shù)。
隱式轉(zhuǎn)換
@ParameterizedTest
@ValueSource(strings = "DAYS")
void checkImplicitArgumentConversion(ChronoUnit argument) {
????assertNotNull(argument.name());
}
JUnit5提供了幾個內(nèi)置的隱式類型轉(zhuǎn)換器。轉(zhuǎn)換取決于聲明的方法參數(shù)類型。例如,用@ValueSource(strings = "DAYS")注釋的參數(shù)化測試會隱式轉(zhuǎn)換為類型ChronoUnit的參數(shù)。
回退字符串到對象的轉(zhuǎn)換
@ParameterizedTest
@ValueSource(strings = { "Name1", "Name2" })
void checkImplicitFallbackArgumentConversion(Person person) {
????assertNotNull(person.getName());
}
public class Person {
????private String name;
????public Person(String name) {
????????this.name = name;
????}
????//Getters & Setters
}
JUnit5提供了一個回退機制,用于自動將字符串轉(zhuǎn)換為給定目標(biāo)類型,如果目標(biāo)類型聲明了一個適用的工廠方法或工廠構(gòu)造函數(shù)。例如,用@ValueSource(strings = { "Name1", "Name2" })注釋的參數(shù)化測試可以聲明接受一個類型為Person的參數(shù),其中包含一個名為name且類型為string的單個字段。
顯式轉(zhuǎn)換
@ParameterizedTest
@ValueSource(ints = { 100 })
void checkExplicitArgumentConversion(
????@ConvertWith(StringSimpleArgumentConverter.class) String argument) {
????assertEquals("100", argument);
}
public class StringSimpleArgumentConverter extends SimpleArgumentConverter {
????@Override
????protected Object convert(Object source, Class<?> targetType)
????????throws ArgumentConversionException {
????????return String.valueOf(source);
????}
}
如果由于某種原因,您不想使用隱式參數(shù)轉(zhuǎn)換,則可以使用@ConvertWith注釋來定義自己的參數(shù)轉(zhuǎn)換器。例如,用@ValueSource(ints = { 100 })注釋的參數(shù)化測試可以聲明接受一個類型為String的參數(shù),使用StringSimpleArgumentConverter.class將整數(shù)轉(zhuǎn)換為字符串類型。
參數(shù)聚合
@ArgumentsAccessor
默認(rèn)情況下,提供給@ParameterizedTest方法的每個參數(shù)對應(yīng)于一個方法參數(shù)。因此,當(dāng)提供大量參數(shù)的參數(shù)源可以導(dǎo)致大型方法簽名時,我們可以使用ArgumentsAccessor而不是聲明多個參數(shù)。類型轉(zhuǎn)換支持如上面的隱式轉(zhuǎn)換所述。
@ParameterizedTest
@CsvSource({ "John, 20",
???????? "Harry, 30" })
void checkArgumentsAccessor(ArgumentsAccessor arguments) {
????Person person = new Person(arguments.getString(0),
???????????????????????????? arguments.getInteger(1));
????assertTrue(person.getAge() > 19, person.getName() + " is a teenager");
}
自定義聚合器
我們看到ArgumentsAccessor可以直接訪問@ParameterizedTest方法的參數(shù)。如果我們想在多個測試中聲明相同的ArgumentsAccessor怎么辦?JUnit5通過提供自定義可重用的聚合器來解決此問題。
@AggregateWith
@ParameterizedTest
@CsvSource({ "John, 20",
???????????? "Harry, 30" })
void checkArgumentsAggregator(
????@AggregateWith(PersonArgumentsAggregator.class) Person person) {
????assertTrue(person.getAge() > 19, person.getName() + " is a teenager");
}
public class PersonArgumentsAggregator implements ArgumentsAggregator {
????@Override
????public Object aggregateArguments(ArgumentsAccessor arguments,
????????ParameterContext context) throws ArgumentsAggregationException {
????????return new Person(arguments.getString(0),
arguments.getInteger(1));
????}
}
實現(xiàn)ArgumentsAggregator接口并通過@AggregateWith注釋在@ParameterizedTest方法中注冊它。當(dāng)我們執(zhí)行測試時,它會將聚合結(jié)果作為對應(yīng)測試的參數(shù)提供。ArgumentsAggregator的實現(xiàn)可以是外部類或靜態(tài)嵌套類。
額外福利
由于您已經(jīng)閱讀完文章,我想給您一個額外的福利 - 如果您正在使用像Fluent assertions for java這樣的斷言框架,則可以將java.util.function.Consumer作為參數(shù)傳遞,其中包含斷言本身。
@ParameterizedTest
@MethodSource("checkNumberArgs")
void checkNumber(int number, Consumer<Integer> consumer) {
????consumer.accept(number);????
}
static Stream<Arguments> checkNumberArgs() {????
????Consumer<Integer> evenConsumer =
????????????i -> Assertions.assertThat(i % 2).isZero();
????Consumer<Integer> oddConsumer =
????????????i -> Assertions.assertThat(i % 2).isEqualTo(1);
????return Stream.of(Arguments.of(2, evenConsumer),
???????? Arguments.of(3, oddConsumer));
}
總結(jié)
JUnit5的參數(shù)化測試功能通過消除重復(fù)測試用例的需要,提供多次使用不同輸入運行相同測試的能力,實現(xiàn)了高效的測試。這不僅為開發(fā)團隊節(jié)省了時間和精力,而且還增加了測試過程的覆蓋范圍和有效性。此外,該功能允許對源代碼進行更全面的測試,因為可以使用更廣泛的輸入進行測試,從而增加了識別任何潛在的錯誤或問題的機會??傮w而言,JUnit5的參數(shù)化測試是提高代碼質(zhì)量和可靠性的有價值的工具。
【注】本文譯自: JUnit 5 Parameterized Tests (reflectoring.io)