JUnit 5 –参数化测试
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 { }參數來源
三種成分進行參數化測試:
參數由源提供,可以為測試方法使用任意數量的參數,但至少應有一個(否則測試將根本不會執行)。 存在一些特定的資源,但是您也可以自由創建自己的資源。
要理解的核心概念是:
- 每個源都必須為所有測試方法參數提供參數(因此,第一個參數不能有一個源,第二個參數不能有另一個源)
- 該測試將對每組參數執行一次
價值來源
您已經看到了@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 5 –参数化测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 千骑卷平冈的骑怎么读 骑在千骑卷平冈里咋
- 下一篇: iPhone 15发布后苹果停售了四款i