JUnit 5 –参数化测试
JUnit 5令人印象深刻,尤其是當(dāng)您深入研究擴(kuò)展模型和體系結(jié)構(gòu)時(shí) 。 但是從表面上講,編寫(xiě)測(cè)試的地方,開(kāi)發(fā)的過(guò)程比革命的過(guò)程更具進(jìn)化性 – JUnit 4上沒(méi)有殺手級(jí)功能嗎? 幸運(yùn)的是,至少有一個(gè):參數(shù)化測(cè)試。 JUnit 5對(duì)參數(shù)化測(cè)試方法具有本機(jī)支持,并且具有允許使用同一主題的第三方變體的擴(kuò)展點(diǎn)。 在本文中,我們將研究如何編寫(xiě)參數(shù)化測(cè)試-創(chuàng)建擴(kuò)展將留待將來(lái)使用。
總覽
這篇文章是有關(guān)JUnit 5的系列文章的一部分:
- 設(shè)定
- 基本
- 建筑
- 移民
- 動(dòng)態(tài)測(cè)試
- 參數(shù)化測(cè)試
- 擴(kuò)展模型
- 條件
- 參數(shù)注入
- …
本系列基于預(yù)發(fā)行版本Milestone 4,并且在發(fā)布新的里程碑或GA版本時(shí)會(huì)進(jìn)行更新。 另一個(gè)很好的來(lái)源是《 JUnit 5用戶指南》 。 您可以在GitHub上找到所有代碼示例。
在整個(gè)這篇文章中,我將大量使用terms 參數(shù)和自變量 ,其含義并不相同。 根據(jù)維基百科 :
術(shù)語(yǔ)參數(shù)通常用于指代在函數(shù)定義中找到的變量,而參數(shù)指代傳遞的實(shí)際輸入。
您好,參數(shù)化世界
參數(shù)化測(cè)試入門(mén)非常容易,但是在開(kāi)始樂(lè)趣之前,您必須向項(xiàng)目添加以下依賴項(xiàng):
- 群組ID :org.junit.jupiter
- 工件ID :junit-jupiter-params
- 版本 :5.0.0-M4
然后,通過(guò)在@ParameterizedTest而不是@Test上聲明帶有參數(shù)和拍擊的測(cè)試方法開(kāi)始:
@ParameterizedTest // something's missing - where does `word` come from? void parameterizedTest(String word) {assertNotNull(word); }看起來(lái)不完整– JUnit如何知道參數(shù)字應(yīng)采用哪些參數(shù)? 好吧,因?yàn)槟鸀槠涠x了零參數(shù),所以該方法將被執(zhí)行零次,并且實(shí)際上JUnit報(bào)告了該方法的Empty測(cè)試套件。
為了使事情發(fā)生,您需要提供參數(shù),您可以從中選擇各種來(lái)源。 可以說(shuō),最簡(jiǎn)單的方法是@ValueSource:
@ParameterizedTest @ValueSource(strings = { "Hello", "JUnit" }) void withValueSource(String word) {assertNotNull(word); }確實(shí),現(xiàn)在測(cè)試執(zhí)行了兩次:一次是“ Hello”,一次是“ JUnit”。 在IntelliJ中,如下所示:
這就是開(kāi)始進(jìn)行參數(shù)化測(cè)試所需的一切!
對(duì)于現(xiàn)實(shí)生活中的使用,您應(yīng)該了解@ParamterizedTest的來(lái)龍去脈(例如,如何命名),其他參數(shù)來(lái)源(包括如何創(chuàng)建自己的)以及到目前為止的更多知識(shí)。有點(diǎn)神秘的功能,稱為參數(shù)轉(zhuǎn)換器。 我們現(xiàn)在將研究所有這些。
參數(shù)化測(cè)試的來(lái)龍去脈
使用@ParameterizedTests創(chuàng)建測(cè)試很簡(jiǎn)單,但是要充分利用該功能,您需要了解一些細(xì)節(jié)。
測(cè)試名稱
從上面的IntelliJ屏幕截圖可以看出,參數(shù)化的測(cè)試方法顯示為帶有每個(gè)調(diào)用的子節(jié)點(diǎn)的測(cè)試容器。 這些節(jié)點(diǎn)的名稱默認(rèn)為“ [{index}] {arguments}”,但可以使用@ParameterizedTest設(shè)置其他名稱:
@ParameterizedTest(name = "run #{index} with [{arguments}]") @ValueSource(strings = { "Hello", "JUnit" }) void withValueSource(String word) { }只要修剪后的字符串不為空,就可以將其用作測(cè)試的名稱。 可以使用以下占位符:
- {index}:從1開(kāi)始計(jì)數(shù)測(cè)試方法的調(diào)用; 此占位符被替換為當(dāng)前調(diào)用的索引
- {arguments}:被方法的n個(gè)參數(shù)替換為{0},{1},…{n}(到目前為止,我們僅看到帶有一個(gè)參數(shù)的方法)
- {i}:被當(dāng)前調(diào)用中第i個(gè)參數(shù)具有的參數(shù)替換
我們將在一分鐘內(nèi)介紹替代資源,因此暫時(shí)忽略@CsvSource的詳細(xì)信息。 只需看看可以通過(guò)這種方式構(gòu)建的出色測(cè)試名稱,尤其是與@DisplayName一起使用 :
@DisplayName("Roman numeral") @ParameterizedTest(name = "\"{0}\" should be {1}") @CsvSource({ "I, 1", "II, 2", "V, 5"}) void withNiceName(String word, int number) { }
非參數(shù)化參數(shù)
不管參數(shù)化測(cè)試如何,JUnit Jupiter都已經(jīng)可以將參數(shù)注入測(cè)試方法中 。 只要將每次調(diào)用中變化的參數(shù)排在首位,這可以與參數(shù)化測(cè)試結(jié)合使用:
@ParameterizedTest @ValueSource(strings = { "Hello", "JUnit" }) void withOtherParams(String word, TestInfo info, TestReporter reporter) {reporter.publishEntry(info.getDisplayName(), "Word: " + word); }與以前一樣,此方法被調(diào)用兩次,兩次參數(shù)解析器都必須提供TestInfo和TestReporter的實(shí)例。 在這種情況下,這些提供程序已內(nèi)置在Jupiter中,但是自定義提供程序(例如用于模擬)也將同樣有效。
元注釋
最后但并非最不重要的一點(diǎn)是,@ParameterizedTest(以及所有源代碼)可以用作元注釋來(lái)創(chuàng)建自定義擴(kuò)展和注釋 :
@Params void testMetaAnnotation(String s) { }@Retention(RetentionPolicy.RUNTIME) @ParameterizedTest(name = "Elaborate name listing all {arguments}") @ValueSource(strings = { "Hello", "JUnit" }) @interface Params { }參數(shù)來(lái)源
三種成分進(jìn)行參數(shù)化測(cè)試:
參數(shù)由源提供,可以為測(cè)試方法使用任意數(shù)量的參數(shù),但至少應(yīng)有一個(gè)(否則測(cè)試將根本不會(huì)執(zhí)行)。 存在一些特定的資源,但是您也可以自由創(chuàng)建自己的資源。
要理解的核心概念是:
- 每個(gè)源都必須為所有測(cè)試方法參數(shù)提供參數(shù)(因此,第一個(gè)參數(shù)不能有一個(gè)源,第二個(gè)參數(shù)不能有另一個(gè)源)
- 該測(cè)試將對(duì)每組參數(shù)執(zhí)行一次
價(jià)值來(lái)源
您已經(jīng)看到了@ValueSource的實(shí)際應(yīng)用。 它使用起來(lái)非常簡(jiǎn)單,并且可以為幾種基本類型輸入安全類型。 您只需應(yīng)用注釋,然后從以下元素之一(也可以是其中一個(gè))中進(jìn)行選擇:
- String [] strings()
- int [] ints()
- long [] longs()
- double [] doubles()
之前,我向您展示了字符串–在這里,您已經(jīng)花費(fèi)了很長(zhǎng)時(shí)間:
@ParameterizedTest @ValueSource(longs = { 42, 63 }) void withValueSource(long number) { }有兩個(gè)主要缺點(diǎn):
- 由于Java對(duì)有效元素類型的限制 ,它不能用于提供任意對(duì)象(盡管對(duì)此有一種補(bǔ)救方法-請(qǐng)等到閱讀有關(guān)參數(shù)轉(zhuǎn)換器的信息之后 )
- 它只能用于具有單個(gè)參數(shù)的測(cè)試方法
因此,對(duì)于大多數(shù)非平凡的用例,您將不得不使用其他來(lái)源之一。
枚舉來(lái)源
這是一個(gè)非常具體的資源,您可以使用它為一個(gè)枚舉或其子集的每個(gè)值運(yùn)行一次測(cè)試:
@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 }直截了當(dāng)吧? 但是請(qǐng)注意,@ EnumSource只為一個(gè)參數(shù)創(chuàng)建參數(shù),這與源必須為每個(gè)參數(shù)提供參數(shù)的事實(shí)相結(jié)合,這意味著它只能在單參數(shù)方法上使用。
方法來(lái)源
@ValueSource和@EnumSource非常簡(jiǎn)單,并且在一定程度上受到了限制–一般方法的另一端是@MethodSource。 它只是簡(jiǎn)單地命名將提供參數(shù)流的方法。 從字面上看:
@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是一個(gè)包裝對(duì)象數(shù)組的簡(jiǎn)單接口,ObjectArrayArguments.create(Object…args)從提供給它的varargs創(chuàng)建它的實(shí)例。 支持注釋的類完成了其余工作,并且withMethodSource這樣執(zhí)行了兩次:一次用word =“ Hello” / length = 5,一次用word =“ JUnit 5” / length = 7。
@MethodSource命名的方法必須是靜態(tài)的,并且可以是私有的。 他們必須返回一種集合,該集合可以是任何Stream(包括原始的特殊性),Iterable,Iterator或數(shù)組。
如果源僅用于單個(gè)參數(shù),則可能空白返回此類實(shí)例,而不將其包裝在Argument中:
@ParameterizedTest @MethodSource(names = "createWords") void withMethodSource(String word) { }private static Stream createWords() {return Stream.of("Hello", "Junit"); }就像我說(shuō)的那樣,@ MethodSource是Jupiter提供的最通用的資源。 但這會(huì)招致聲明方法和將參數(shù)組合在一起的開(kāi)銷,這對(duì)于較簡(jiǎn)單的情況來(lái)說(shuō)有點(diǎn)多。 最好使用兩個(gè)CSV來(lái)源。
CSV來(lái)源
現(xiàn)在,它變得非常有趣。 能夠在那時(shí)和那里為幾個(gè)參數(shù)定義少數(shù)參數(shù)集而不必通過(guò)聲明方法來(lái)很好嗎? 輸入@CsvSource! 使用它,您可以將每次調(diào)用的參數(shù)聲明為以逗號(hào)分隔的字符串列表,并將其余參數(shù)留給JUnit:
@ParameterizedTest @CsvSource({ "Hello, 5", "JUnit 5, 7", "'Hello, JUnit 5!', 15" }) void withCsvSource(String word, int length) { }在此示例中,源標(biāo)識(shí)了三組參數(shù),從而導(dǎo)致了三個(gè)測(cè)試調(diào)用,然后繼續(xù)將它們放在逗號(hào)上并將其轉(zhuǎn)換為目標(biāo)類型。 看到“'Hello,JUnit 5!',15”中的單引號(hào)嗎? 這是使用逗號(hào)的方式,而不會(huì)在該位置將字符串切成兩半。
將所有參數(shù)都表示為字符串會(huì)引起一個(gè)問(wèn)題,即如何將它們轉(zhuǎn)換為正確的類型。 我們待會(huì)兒會(huì)談,但是在我想快速指出之前,如果您有大量輸入數(shù)據(jù),則可以將它們自由存儲(chǔ)在外部文件中:
@ParameterizedTest @CsvFileSource(resources = "word-lengths.csv") void withCsvSource(String word, int length) { }請(qǐng)注意,資源可以接受多個(gè)文件名,并將一個(gè)接一個(gè)地處理它們。 @CsvFileSource的其他元素允許指定文件的編碼,行分隔符和定界符。
自定義參數(shù)來(lái)源
如果JUnit內(nèi)置的源代碼無(wú)法滿足您的所有用例,則可以自由創(chuàng)建自己的用例。 我將不贅述-足以說(shuō)明,您必須實(shí)現(xiàn)此接口…
public interface ArgumentsProvider {Stream<? extends Arguments> provideArguments(ContainerExtensionContext context) throws Exception;}…,然后將您的源代碼與@ArgumentsSource(MySource.class)或自定義注釋一起使用 。 您可以使用擴(kuò)展上下文訪問(wèn)各種信息,例如,調(diào)用源的方法,以便知道它有多少個(gè)參數(shù)。
現(xiàn)在,開(kāi)始轉(zhuǎn)換這些參數(shù)!
參數(shù)轉(zhuǎn)換器
除了方法源之外,參數(shù)源只能提供非常有限的類型類型:字符串,枚舉和一些基元。 當(dāng)然,這不足以編寫(xiě)全面的測(cè)試,因此需要一條通往更豐富的類型環(huán)境的道路。 參數(shù)轉(zhuǎn)換器就是那條路:
@ParameterizedTest @CsvSource({ "(0/0), 0", "(0/1), 1", "(1/1), 1.414" }) void convertPointNorm(@ConvertPoint Point point, double norm) { }讓我們看看如何到達(dá)那里……
首先,一般觀察:無(wú)論所提供的參數(shù)和目標(biāo)參數(shù)具有哪種類型,都將始終要求轉(zhuǎn)換器將其轉(zhuǎn)換為另一種。 但是,只有前面的示例聲明了一個(gè)轉(zhuǎn)換器,那么在所有其他情況下會(huì)發(fā)生什么?
默認(rèn)轉(zhuǎn)換器
Jupiter提供了一個(gè)默認(rèn)轉(zhuǎn)換器,如果未應(yīng)用其他轉(zhuǎn)換器,則將使用它。 如果參數(shù)和參數(shù)類型匹配,則轉(zhuǎn)換為空操作,但如果參數(shù)為字符串,則可以將其轉(zhuǎn)換為多種目標(biāo)類型:
- char或Character(如果字符串的長(zhǎng)度為1)(如果您使用UTF-32字符(如表情符號(hào),因?yàn)樗鼈儼瑑蓚€(gè)Java字符),則可能會(huì)使您失望)
- 其他所有原語(yǔ)及其包裝類型以及它們各自的valueOf方法
- 通過(guò)使用字符串和目標(biāo)枚舉調(diào)用Enum :: valueOf來(lái)獲取任何枚舉
- 一堆時(shí)間類型,例如Instant,LocalDateTime等,OffsetDateTime等,ZonedDateTime,Year和YearMonth及其各自的解析方法
這是一個(gè)簡(jiǎn)單的示例,其中顯示了其中一些操作:
@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; }受支持的類型的列表可能會(huì)隨著時(shí)間的推移而增長(zhǎng),但是很明顯它不能包括特定于您的代碼庫(kù)的類型。 這是定制轉(zhuǎn)換器輸入圖片的地方。
定制轉(zhuǎn)換器
使用自定義轉(zhuǎn)換器,您可以將源發(fā)出的參數(shù)(通常是字符串)轉(zhuǎn)換為要在測(cè)試中使用的任意類型的實(shí)例。 創(chuàng)建它們很容易–您所需要做的就是實(shí)現(xiàn)ArgumentConverter接口:
public interface ArgumentConverter {Object convert(Object input, ParameterContext context)throws ArgumentConversionException;}輸入和輸出是無(wú)類型的,這有點(diǎn)令人討厭,但是,因?yàn)镴upiter知道兩者都不是,所以在更具體的方面確實(shí)沒(méi)有用。 您可以使用參數(shù)上下文獲取有關(guān)要為其提供參數(shù)的參數(shù)的更多信息,例如參數(shù)的類型或最終將在其上調(diào)用測(cè)試方法的實(shí)例。
對(duì)于已經(jīng)具有靜態(tài)工廠方法(例如“(1/0)”)的Point類,convert方法非常簡(jiǎn)單:
@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的第一個(gè)檢查輸入實(shí)例有點(diǎn)麻木(為什么它已經(jīng)是一個(gè)點(diǎn)了?),但是一旦我開(kāi)始打開(kāi)類型,就無(wú)法讓自己忽略這種情況。 隨時(shí)判斷我。
現(xiàn)在,您可以使用@ConvertWith應(yīng)用轉(zhuǎn)換器:
@ParameterizedTest @ValueSource(strings = { "(0/0)", "(0/1)","(1/1)" }) void convertPoint(@ConvertWith(PointConverter.class) Point point) { }或者,您可以創(chuàng)建一個(gè)自定義批注以使其看起來(lái)不那么技術(shù):
@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 { }這意味著,通過(guò)使用@ConvertWith或自定義注釋對(duì)參數(shù)進(jìn)行注釋,JUnit Jupiter將傳遞提供給轉(zhuǎn)換器的源的任何參數(shù)。 通常,您會(huì)將其應(yīng)用于發(fā)出字符串的@ValueSource或@CsvSource之類的源,以便隨后將其解析為您選擇的對(duì)象。
反射
那是一個(gè)很大的旅程,所以讓我們確保我們擁有一切:
- 我們首先添加了junit-jupiter-params工件,然后將@ParameterizedTest應(yīng)用于帶有參數(shù)的測(cè)試方法。 在研究了如何命名參數(shù)化測(cè)試之后,我們開(kāi)始討論參數(shù)的來(lái)源。
- 第一步是使用@ ValueSource,@ MethodSource或@CsvSource之類的源來(lái)為該方法創(chuàng)建參數(shù)組。 每個(gè)組都必須具有所有參數(shù)的參數(shù)(參數(shù)解析器中的參數(shù)除外),并且每個(gè)組將調(diào)用該方法一次。 可以實(shí)現(xiàn)自定義源并將其與@ArgumentsSource一起應(yīng)用。
- 由于源通常僅限于幾種基本類型,因此第二步是將它們轉(zhuǎn)換為任意類型。 默認(rèn)轉(zhuǎn)換器對(duì)原語(yǔ),枚舉和某些日期/時(shí)間類型執(zhí)行此操作。 定制轉(zhuǎn)換器可以與@ConvertWith一起應(yīng)用。
這使您可以輕松地使用JUnit Jupiter參數(shù)化您的測(cè)試!
但是,這種特定機(jī)制很可能無(wú)法滿足您的所有需求。 在這種情況下,您會(huì)很高興聽(tīng)到它是通過(guò)擴(kuò)展點(diǎn)實(shí)現(xiàn)的,可用于創(chuàng)建您自己的參數(shù)化測(cè)試的變體–我將在以后的文章中進(jìn)行研究,敬請(qǐng)期待。
翻譯自: https://www.javacodegeeks.com/2017/06/junit-5-parameterized-tests.html
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的JUnit 5 –参数化测试的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 千骑卷平冈的骑怎么读 骑在千骑卷平冈里咋
- 下一篇: eclipse中junit_在Eclip