参数化测试 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