參數化測試 junit
JUnit 5令人印象深刻,尤其是當您深入研究擴充模型和體系結構時 。 但是從表面上講,編寫測試的地方,開發的過程比革命的過程更具進化性 – JUnit 4上沒有殺手級功能嗎? 幸運的是,至少有一個:參數化測試。 JUnit 5具有參數化測試方法的本機支援,以及一個擴充點,該擴充點允許使用同一主題的第三方變體。 在本文中,我們将研究如何編寫參數化測試-建立擴充将留待将來使用。
總覽
這篇文章是有關JUnit 5的系列文章的一部分:
- 建立
- 基本
- 建築
- 移民
- 動态測試
- 參數化測試
- 擴充模型
- 條件
- 參數注入
- …
本系列基于預釋出版本Milestone 4,并且在釋出新的裡程碑或GA版本時會進行更新。 另一個很好的來源是《 JUnit 5使用者指南》 。 您可以在GitHub上找到所有代碼示例。
在整個這篇文章中,我将大量使用terms 參數和自變量,并且它們的含義并不相同。 根據維基百科 :
術語參數通常用于指代在函數定義中找到的變量,而參數指代傳遞的實際輸入。
您好,參數化世界
參數化測試入門非常容易,但是在開始樂趣之前,您必須向項目添加以下依賴項:
- 群組ID :org.junit.jupiter
- 工件ID :junit-jupiter-params
- 版本 :5.0.0-M4
然後,通過在@ParameterizedTest而不是@Test上聲明帶有參數和拍擊的測試方法開始:
@ParameterizedTest
// something's missing - where does `word` come from?
void parameterizedTest(String word) {
assertNotNull(word);
}
它看起來不完整– JUnit如何知道參數字應采用哪些參數? 好吧,由于您為它定義了零個參數,是以該方法将被執行零次,并且JUnit實際上報告了該方法的Empty測試套件。
為了使事情發生,您需要提供參數,您可以從中選擇各種來源。 可以說,最簡單的方法是@ValueSource:
@ParameterizedTest
@ValueSource(strings = { "Hello", "JUnit" })
void withValueSource(String word) {
assertNotNull(word);
}
确實,現在測試執行了兩次:一次是“ Hello”,一次是“ JUnit”。 在IntelliJ中,如下所示:
這就是開始進行參數化測試所需的一切!
對于現實生活,您應該了解@ParamterizedTest的來龍去脈(例如,如何命名它們),其他參數來源(包括如何建立自己的參數)以及到目前為止的更多知識。有點神秘的功能,稱為參數轉換器。 我們現在将研究所有這些。
參數化測試的來龍去脈
使用@ParameterizedTests建立測試很簡單,但是要充分利用該功能,有一些細節是很好的。
測試名稱
從上面的IntelliJ螢幕截圖可以看出,參數化測試方法顯示為帶有每次調用的子節點的測試容器。 這些節點的名稱預設為“ [[{index}] {arguments}”,但可以使用@ParameterizedTest設定其他名稱:
@ParameterizedTest(name = "run #{index} with [{arguments}]")
@ValueSource(strings = { "Hello", "JUnit" })
void withValueSource(String word) { }
隻要修剪後的字元串不為空,就可以将其用作測試的名稱。 可以使用以下占位符:
- {index}:從1開始計數測試方法的調用; 該占位符被替換為目前調用的索引
- {arguments}:被方法的n個參數替換為{0},{1},…{n}(到目前為止,我們僅看到帶有一個參數的方法)
- {i}:被目前調用中第i個參數具有的參數替換
我們将在一分鐘内介紹替代資源,是以暫時忽略@CsvSource的詳細資訊。 隻需看看可以通過這種方式建構的出色測試名稱,尤其是與@DisplayName一起使用 :
@DisplayName("Roman numeral")
@ParameterizedTest(name = "\"{0}\" should be {1}")
@CsvSource({ "I, 1", "II, 2", "V, 5"})
void withNiceName(String word, int number) { }
非參數化參數
不管參數化測試如何,JUnit Jupiter都已經可以将參數注入測試方法中 。 隻要将每次調用中變化的參數排在首位,這可以與參數化測試結合使用:
@ParameterizedTest
@ValueSource(strings = { "Hello", "JUnit" })
void withOtherParams(String word, TestInfo info, TestReporter reporter) {
reporter.publishEntry(info.getDisplayName(), "Word: " + word);
}
與以前一樣,此方法被調用兩次,兩次參數解析器都必須提供TestInfo和TestReporter的執行個體。 在這種情況下,這些提供程式已内置在Jupiter中,但是自定義提供程式(例如用于模拟)也将同樣有效。
元注釋
最後但并非最不重要的一點是,@ParameterizedTest(以及所有源代碼)可以用作元注釋來建立自定義擴充和注釋 :
@Params
void testMetaAnnotation(String s) { }
@Retention(RetentionPolicy.RUNTIME)
@ParameterizedTest(name = "Elaborate name listing all {arguments}")
@ValueSource(strings = { "Hello", "JUnit" })
@interface Params { }
參數來源
三種成分進行參數化測試:
- 有參數的方法
- @ParameterizedTest批注
- 參數值,即參數
參數由源提供,可以為測試方法使用任意數量的參數,但至少應有一個(否則測試将根本不會執行)。 存在一些特定的資源,但是您也可以自由建立自己的資源。
要了解的核心概念是:
- 每個來源都必須為所有測試方法參數提供參數(是以第一個參數不能有一個來源,第二個參數不能有另一個來源)
- 該測試将對每組參數執行一次
價值來源
您已經看到了@ValueSource的實際應用。 它使用起來非常簡單,并且可以為幾種基本類型輸入安全類型。 您隻需應用注釋,然後從以下元素之一(也可以是其中一個)中進行選擇:
- String [] strings()
- int [] ints()
- long [] longs()
- double [] doubles()
之前,我向您展示了字元串–在這裡,您已經花費了很長時間
@ParameterizedTest
@ValueSource(longs = { 42, 63 })
void withValueSource(long number) { }
有兩個主要缺點:
- 由于Java對有效元素類型的限制 ,它不能用于提供任意對象(盡管對此有一種補救方法-請等到您了解參數轉換器之後再進行選擇 )
- 它隻能用于具有單個參數的測試方法
是以,對于大多數非平凡的用例,您将不得不使用其他來源之一。
枚舉來源
這是一個非常具體的資源,您可以使用它對一個枚舉或其子集的每個值運作一次測試:
@ParameterizedTest
@EnumSource(TimeUnit.class)
void withAllEnumValues(TimeUnit unit) {
// executed once for each time unit
}
@ParameterizedTest
@EnumSource(
value = TimeUnit.class,
names = {"NANOSECONDS", "MICROSECONDS"})
void withSomeEnumValues(TimeUnit unit) {
// executed once for TimeUnit.NANOSECONDS
// and once for TimeUnit.MICROSECONDS
}
直截了當吧? 但是請注意,@ EnumSource隻為一個參數建立參數,這與源必須為每個參數提供參數的事實相結合,這意味着它隻能在單參數方法上使用。
方法來源
@ValueSource和@EnumSource非常簡單,并且有些局限性–在通用範圍的另一端是@MethodSource。 它隻是簡單地命名将提供參數流的方法。 從字面上看:
@ParameterizedTest
@MethodSource(names = "createWordsWithLength")
void withMethodSource(String word, int length) { }
private static Stream createWordsWithLength() {
return Stream.of(
ObjectArrayArguments.create("Hello", 5),
ObjectArrayArguments.create("JUnit 5", 7));
}
Argument是一個包裝對象數組的簡單接口,ObjectArrayArguments.create(Object…args)從提供給它的varargs建立它的執行個體。 支援注釋的類完成了其餘工作,并且withMethodSource這樣執行了兩次:一次用word =“ Hello” / length = 5,一次用word =“ JUnit 5” / length = 7。
@MethodSource命名的方法必須是靜态的,并且可以是私有的。 他們必須傳回一種集合,該集合可以是任何Stream(包括原始的特殊化),Iterable,Iterator或數組。
如果源僅用于單個參數,則可能空白傳回此類執行個體而不将其包裝在Argument中:
@ParameterizedTest
@MethodSource(names = "createWords")
void withMethodSource(String word) { }
private static Stream createWords() {
return Stream.of("Hello", "Junit");
}
就像我說的那樣,@ MethodSource是Jupiter提供的最通用的資源。 但這會招緻聲明方法和将參數組合在一起的開銷,這對于較簡單的情況來說有點多。 最好使用兩個CSV來源。
CSV來源
現在,它變得非常有趣。 能夠在那時和那裡為幾個參數定義少數參數集而不必通過聲明方法來很好嗎? 輸入@CsvSource! 使用它,您可以将每次調用的參數聲明為以逗号分隔的字元串清單,并将其餘參數留給JUnit:
@ParameterizedTest
@CsvSource({ "Hello, 5", "JUnit 5, 7", "'Hello, JUnit 5!', 15" })
void withCsvSource(String word, int length) { }
在此示例中,源辨別了三組參數,進而導緻了三個測試調用,然後繼續将它們分開(以逗号分隔)并将其轉換為目标類型。 看到“'Hello,JUnit 5!',15”中的單引号嗎? 這是使用逗号的方式,而不會在該位置将字元串切成兩半。
将所有參數都表示為字元串會引起一個問題,即如何将它們轉換為正确的類型。 我們待會兒會談,但是在我想快速指出之前,如果您有大量輸入資料,可以将它們自由存儲在外部檔案中:
@ParameterizedTest
@CsvFileSource(resources = "word-lengths.csv")
void withCsvSource(String word, int length) { }
請注意,資源可以接受多個檔案名,并将一個接一個地處理它們。 @CsvFileSource的其他元素允許指定檔案的編碼,行分隔符和定界符。
自定義參數來源
如果JUnit内置的源代碼無法滿足您的所有用例,則可以自由建立自己的用例。 我将不贅述-足以說明,您必須實作此接口…
public interface ArgumentsProvider {
Stream<? extends Arguments> provideArguments(
ContainerExtensionContext context) throws Exception;
}
…,然後将您的源代碼與@ArgumentsSource(MySource.class)或自定義注釋一起使用 。 您可以使用擴充上下文通路各種資訊,例如,調用源的方法,以便知道它有多少個參數。
現在,開始轉換這些參數!
參數轉換器
除了方法源之外,參數源提供的類型的種類非常有限:僅字元串,枚舉和一些基元。 當然,這不足以編寫全面的測試,是以需要一條通往更豐富的類型環境的道路。 參數轉換器就是那條路:
@ParameterizedTest
@CsvSource({ "(0/0), 0", "(0/1), 1", "(1/1), 1.414" })
void convertPointNorm(@ConvertPoint Point point, double norm) { }
讓我們看看如何到達那裡……
首先,一般觀察:無論提供的參數和目标參數具有什麼類型,都将始終要求轉換器将其轉換為另一種。 但是,隻有前面的示例聲明了一個轉換器,那麼在所有其他情況下會發生什麼?
預設轉換器
Jupiter提供了一個預設轉換器,如果未應用其他轉換器,則将使用它。 如果參數和參數類型比對,則轉換為空操作,但如果參數為字元串,則可以将其轉換為多種目标類型:
- char或Character(如果字元串的長度為1)(如果您使用UTF-32字元(如表情符号,因為它們包含兩個Java字元),則可能會使您失望)
- 其他所有原語及其包裝類型以及它們各自的valueOf方法
- 通過使用字元串和目标枚舉調用Enum :: valueOf來擷取任何枚舉
- 一堆時間類型,例如Instant,LocalDateTime等,OffsetDateTime等,ZonedDateTime,Year和YearMonth及其各自的解析方法
這是一個簡單的示例,其中顯示了其中一些操作:
@ParameterizedTest
@CsvSource({"true, 3.14159265359, JUNE, 2017, 2017-06-21T22:00:00"})
void testDefaultConverters(
boolean b, double d, Summer s, Year y, LocalDateTime dt) { }
enum Summer {
JUNE, JULY, AUGUST, SEPTEMBER;
}
受支援的類型的清單可能會随着時間的推移而增長,但是很明顯,它不能包括特定于您的代碼庫的類型。 這是定制轉換器輸入圖檔的地方。
定制轉換器
使用自定義轉換器,您可以将源發出的參數(通常是字元串)轉換為要在測試中使用的任意類型的執行個體。 建立它們很容易–您所需要做的就是實作ArgumentConverter接口:
public interface ArgumentConverter {
Object convert(
Object input, ParameterContext context)
throws ArgumentConversionException;
}
輸入和輸出是無類型的,這有點令人讨厭,但是,由于Jupiter都不知道兩者的類型,是以在進行更具體的輸入方面實在沒有用。 您可以使用參數上下文擷取有關要為其提供參數的參數的更多資訊,例如參數的類型或最終将在其上調用測試方法的執行個體。
對于已經具有靜态工廠方法(例如“(1/0)”)的Point類,convert方法非常簡單:
@Override
public Object convert(
Object input, ParameterContext parameterContext)
throws ArgumentConversionException {
if (input instanceof Point)
return input;
if (input instanceof String)
try {
return Point.from((String) input);
} catch (NumberFormatException ex) {
String message = input
+ " is no correct string representation of a point.";
throw new ArgumentConversionException(message, ex);
}
throw new ArgumentConversionException(input + " is no valid point");
}
Point的第一個檢查輸入執行個體有點麻木(為什麼它已經是一個點了?),但是一旦我開始打開類型,便無法使自己忽略這種情況。 随時判斷我。
現在,您可以使用@ConvertWith應用轉換器:
@ParameterizedTest
@ValueSource(strings = { "(0/0)", "(0/1)","(1/1)" })
void convertPoint(@ConvertWith(PointConverter.class) Point point) { }
或者,您可以建立一個自定義批注以使其看起來不太技術:
@ParameterizedTest
@ValueSource(strings = { "(0/0)", "(0/1)","(1/1)" })
void convertPoint(@ConvertPoint Point point) { }
@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ConvertWith(PointConverter.class)
@interface ConvertPoint { }
這意味着通過使用@ConvertWith或自定義注釋對參數進行注釋,JUnit Jupiter将傳遞提供給轉換器的源的任何參數。 通常,您會将其應用于發出字元串的@ValueSource或@CsvSource之類的源,以便随後将其解析為您選擇的對象。
反射
那是一個很大的旅程,是以讓我們確定我們擁有一切:
- 我們首先添加了junit-jupiter-params工件,然後将@ParameterizedTest應用于帶有參數的測試方法。 在研究了如何命名參數化測試之後,我們開始讨論參數的來源。
- 第一步是使用@ ValueSource,@ MethodSource或@CsvSource之類的源來為方法建立參數組。 每個組都必須具有所有參數的參數(參數解析器中的參數除外),并且每個組将調用該方法一次。 可以實作自定義源并将其與@ArgumentsSource一起應用。
- 由于源通常僅限于幾種基本類型,是以第二步是将它們轉換為任意類型。 預設轉換器對基元,枚舉和某些日期/時間類型執行此操作; 定制轉換器可以與@ConvertWith一起應用。
這使您可以輕松地使用JUnit Jupiter參數化您的測試!
但是,這種特定機制很可能無法滿足您的所有需求。 在這種情況下,您會很高興聽到它是通過擴充點實作的,可用于建立您自己的參數化測試的變體–我将在以後的文章中對此進行探讨,請繼續關注。
翻譯自: https://www.javacodegeeks.com/2017/06/junit-5-parameterized-tests.html
參數化測試 junit