当前位置:   article > 正文

JUnit5参数化测试的几种方式!

JUnit5参数化测试的几种方式!

参数化测试一直是津津乐道的话题,我们都知道JMeter有四种参数化方式:用户自定义变量、用户参数、CSV文件、函数助手,那么JUnit5有哪些参数化测试的方式呢?

依赖

JUnit5需要添加junit-jupiter-params依赖才能使用参数化

  1. <dependency>
  2. <groupId>org.junit.jupiter</groupId>
  3. <artifactId>junit-jupiter-params</artifactId>
  4. <version>5.7.2</version>
  5. <scope>test</scope>
  6. </dependency>

简单示例

@ParameterizedTest用来定义参数化测试,@ValueSource用来定义参数值:

  1. @ParameterizedTest
  2. @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
  3. void palindromes(String candidate) {
  4. assertTrue(StringUtils.isPalindrome(candidate));
  5. }

执行结果:

  1. palindromes(String) ✔
  2. ├─ [1] candidate=racecar ✔
  3. ├─ [2] candidate=radar ✔
  4. └─ [3] candidate=able was I ere I saw elba ✔

参数值会匹配测试方法的参数列表,然后依次赋值,这里一共产生了3个测试。

七种方式

@ValueSource

@ValueSource是最简单的参数化方式,它是一个数组,支持以下数据类型:

  • short
  • byte
  • int
  • long
  • float
  • double
  • char
  • boolean
  • java.lang.String
  • java.lang.Class

示例:

  1. @ParameterizedTest
  2. @ValueSource(ints = { 1, 2, 3 })
  3. void testWithValueSource(int argument) {
  4. assertTrue(argument > 0 && argument < 4);
  5. }

2 Null and Empty Sources

  • @NullSource 值为null

    不能用在基元类型的测试方法。

  • @EmptySource 值为空,根据测试方法的参数类决定数据类型,支持java.lang.Stringjava.util.Listjava.util.Setjava.util.Map, 基元类型数组 (int[]char[][]等), 对象数组 (String[]Integer[][]等)

  • @NullAndEmptySource 结合了前面两个

示例:

  1. @ParameterizedTest
  2. @NullSource
  3. @EmptySource
  4. @ValueSource(strings = { " ", " ", "\t", "\n" })
  5. void nullEmptyAndBlankStrings(String text) {
  6. assertTrue(text == null || text.trim().isEmpty());
  7. }

等价于:

  1. @ParameterizedTest
  2. @NullAndEmptySource
  3. @ValueSource(strings = { " ", " ", "\t", "\n" })
  4. void nullEmptyAndBlankStrings(String text) {
  5. assertTrue(text == null || text.trim().isEmpty());
  6. }

@EnumSource

参数化的值为枚举类型。

示例:

  1. @ParameterizedTest
  2. @EnumSource
  3. void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
  4. assertNotNull(unit);
  5. }

其中的ChronoUnit是个日期枚举类。

ChronoUnit是接口TemporalUnit的实现类,如果测试方法的参数为TemporalUnit,那么需要给@EnumSource加上值:

  1. @ParameterizedTest
  2. @EnumSource(ChronoUnit.class)
  3. void testWithEnumSource(TemporalUnit unit) {
  4. assertNotNull(unit);
  5. }

因为JUnit5规定了@EnumSource的默认值的类型必须是枚举类型。

names属性用来指定使用哪些特定的枚举值:

  1. @ParameterizedTest
  2. @EnumSource(names = { "DAYS", "HOURS" })
  3. void testWithEnumSourceInclude(ChronoUnit unit) {
  4. assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
  5. }

mode属性用来指定使用模式,比如排除哪些枚举值:

  1. @ParameterizedTest
  2. @EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
  3. void testWithEnumSourceExclude(ChronoUnit unit) {
  4. assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
  5. }

比如采用正则匹配:

  1. @ParameterizedTest
  2. @EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
  3. void testWithEnumSourceRegex(ChronoUnit unit) {
  4. assertTrue(unit.name().endsWith("DAYS"));
  5. }
  1. 现在我也找了很多测试的朋友,做了一个分享技术的交流群,共享了很多我们收集的技术文档和视频教程。
  2. 如果你不想再体验自学时找不到资源,没人解答问题,坚持几天便放弃的感受
  3. 可以加入我们一起交流。而且还有很多在自动化,性能,安全,测试开发等等方面有一定建树的技术大牛
  4. 分享他们的经验,还会分享很多直播讲座和技术沙龙
  5. 可以免费学习!划重点!开源的!!!
  6. qq群号:110685036

@MethodSource

参数值为factory方法,并且factory方法不能带参数。

示例:

  1. @ParameterizedTest
  2. @MethodSource("stringProvider")
  3. void testWithExplicitLocalMethodSource(String argument) {
  4. assertNotNull(argument);
  5. }
  6. static Stream<String> stringProvider() {
  7. return Stream.of("apple", "banana");
  8. }

除非是@TestInstance(Lifecycle.PER_CLASS)生命周期,否则factory方法必须是static。factory方法的返回值是能转换为Stream的类型,比如StreamDoubleStreamLongStreamIntStreamCollectionIteratorIterable, 对象数组, 或者基元类型数组,比如:

  1. @ParameterizedTest
  2. @MethodSource("range")
  3. void testWithRangeMethodSource(int argument) {
  4. assertNotEquals(9, argument);
  5. }
  6. static IntStream range() {
  7. return IntStream.range(0, 20).skip(10);
  8. }

@MethodSource的属性如果省略了,那么JUnit Jupiter会找跟测试方法同名的factory方法,比如:

  1. @ParameterizedTest
  2. @MethodSource
  3. void testWithDefaultLocalMethodSource(String argument) {
  4. assertNotNull(argument);
  5. }
  6. static Stream<String> testWithDefaultLocalMethodSource() {
  7. return Stream.of("apple", "banana");
  8. }

如果测试方法有多个参数,那么factory方法也应该返回多个:

  1. @ParameterizedTest
  2. @MethodSource("stringIntAndListProvider")
  3. void testWithMultiArgMethodSource(String str, int num, List<String> list) {
  4. assertEquals(5, str.length());
  5. assertTrue(num >=1 && num <=2);
  6. assertEquals(2, list.size());
  7. }
  8. static Stream<Arguments> stringIntAndListProvider() {
  9. return Stream.of(
  10. arguments("apple", 1, Arrays.asList("a", "b")),
  11. arguments("lemon", 2, Arrays.asList("x", "y"))
  12. );
  13. }

其中arguments(Object…)是Arguments接口的static factory method,也可以换成Arguments.of(Object…)

factory方法也可以防止测试类外部:

  1. package example;
  2. import java.util.stream.Stream;
  3. import org.junit.jupiter.params.ParameterizedTest;
  4. import org.junit.jupiter.params.provider.MethodSource;
  5. class ExternalMethodSourceDemo {
  6. @ParameterizedTest
  7. @MethodSource("example.StringsProviders#tinyStrings")
  8. void testWithExternalMethodSource(String tinyString) {
  9. // test with tiny string
  10. }
  11. }
  12. class StringsProviders {
  13. static Stream<String> tinyStrings() {
  14. return Stream.of(".", "oo", "OOO");
  15. }
  16. }

@CsvSource

参数化的值为csv格式的数据(默认逗号分隔),比如:

  1. @ParameterizedTest
  2. @CsvSource({
  3. "apple, 1",
  4. "banana, 2",
  5. "'lemon, lime', 0xF1"
  6. })
  7. void testWithCsvSource(String fruit, int rank) {
  8. assertNotNull(fruit);
  9. assertNotEquals(0, rank);
  10. }

delimiter属性可以设置分隔字符。delimiterString属性可以设置分隔字符串(String而非char)。

更多输入输出示例如下:

image-20210714140242605

注意,如果null引用的目标类型是基元类型,那么会报异常ArgumentConversionException

@CsvFileSource

顾名思义,选择本地csv文件作为数据来源。

示例:

  1. @ParameterizedTest
  2. @CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
  3. void testWithCsvFileSourceFromClasspath(String country, int reference) {
  4. assertNotNull(country);
  5. assertNotEquals(0, reference);
  6. }
  7. @ParameterizedTest
  8. @CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
  9. void testWithCsvFileSourceFromFile(String country, int reference) {
  10. assertNotNull(country);
  11. assertNotEquals(0, reference);
  12. }

delimiter属性可以设置分隔字符。delimiterString属性可以设置分隔字符串(String而非char)。需要特别注意的是,#开头的行会被认为是注释而略过。

@ArgumentsSource

自定义ArgumentsProvider。

示例:

  1. @ParameterizedTest
  2. @ArgumentsSource(MyArgumentsProvider.class)
  3. void testWithArgumentsSource(String argument) {
  4. assertNotNull(argument);
  5. }
  1. public class MyArgumentsProvider implements ArgumentsProvider {
  2. @Override
  3. public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
  4. return Stream.of("apple", "banana").map(Arguments::of);
  5. }
  6. }

MyArgumentsProvider必须是外部类或者static内部类。

参数类型转换

隐式转换

JUnit Jupiter会对String类型进行隐式转换。比如:

  1. @ParameterizedTest
  2. @ValueSource(strings = "SECONDS")
  3. void testWithImplicitArgumentConversion(ChronoUnit argument) {
  4. assertNotNull(argument.name());
  5. }

更多转换示例:

image-20210714143735484

Dingtalk_20210714143207

也可以把String转换为自定义对象:

  1. @ParameterizedTest
  2. @ValueSource(strings = "42 Cats")
  3. void testWithImplicitFallbackArgumentConversion(Book book) {
  4. assertEquals("42 Cats", book.getTitle());
  5. }
  1. public class Book {
  2. private final String title;
  3. private Book(String title) {
  4. this.title = title;
  5. }
  6. public static Book fromTitle(String title) {
  7. return new Book(title);
  8. }
  9. public String getTitle() {
  10. return this.title;
  11. }
  12. }

JUnit Jupiter会找到Book.fromTitle(String)方法,然后把@ValueSource的值传入进去,进而把String类型转换为Book类型。转换的factory方法既可以是接受单个String参数的构造方法,也可以是接受单个String参数并返回目标类型的普通方法。详细规则如下(官方原文):

image-20210714145601731

显式转换

显式转换需要使用@ConvertWith注解:

  1. @ParameterizedTest
  2. @EnumSource(ChronoUnit.class)
  3. void testWithExplicitArgumentConversion(
  4. @ConvertWith(ToStringArgumentConverter.class) String argument) {
  5. assertNotNull(ChronoUnit.valueOf(argument));
  6. }

并实现ArgumentConverter:

  1. public class ToStringArgumentConverter extends SimpleArgumentConverter {
  2. @Override
  3. protected Object convert(Object source, Class<?> targetType) {
  4. assertEquals(String.class, targetType, "Can only convert to String");
  5. if (source instanceof Enum<?>) {
  6. return ((Enum<?>) source).name();
  7. }
  8. return String.valueOf(source);
  9. }
  10. }

如果只是简单类型转换,实现TypedArgumentConverter即可:

  1. public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {
  2. protected ToLengthArgumentConverter() {
  3. super(String.class, Integer.class);
  4. }
  5. @Override
  6. protected Integer convert(String source) {
  7. return source.length();
  8. }
  9. }

JUnit Jupiter只内置了一个JavaTimeArgumentConverter,通过@JavaTimeConversionPattern使用:

  1. @ParameterizedTest
  2. @ValueSource(strings = { "01.01.2017", "31.12.2017" })
  3. void testWithExplicitJavaTimeConverter(
  4. @JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
  5. assertEquals(2017, argument.getYear());
  6. }

参数聚合

测试方法的多个参数可以聚合为一个ArgumentsAccessor参数,然后通过get来取值,示例:

  1. @ParameterizedTest
  2. @CsvSource({
  3. "Jane, Doe, F, 1990-05-20",
  4. "John, Doe, M, 1990-10-22"
  5. })
  6. void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
  7. Person person = new Person(arguments.getString(0),
  8. arguments.getString(1),
  9. arguments.get(2, Gender.class),
  10. arguments.get(3, LocalDate.class));
  11. if (person.getFirstName().equals("Jane")) {
  12. assertEquals(Gender.F, person.getGender());
  13. }
  14. else {
  15. assertEquals(Gender.M, person.getGender());
  16. }
  17. assertEquals("Doe", person.getLastName());
  18. assertEquals(1990, person.getDateOfBirth().getYear());
  19. }

也可以自定义Aggregator:

  1. public class PersonAggregator implements ArgumentsAggregator {
  2. @Override
  3. public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) {
  4. return new Person(arguments.getString(0),
  5. arguments.getString(1),
  6. arguments.get(2, Gender.class),
  7. arguments.get(3, LocalDate.class));
  8. }
  9. }

然后通过@AggregateWith来使用:

  1. @ParameterizedTest
  2. @CsvSource({
  3. "Jane, Doe, F, 1990-05-20",
  4. "John, Doe, M, 1990-10-22"
  5. })
  6. void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
  7. // perform assertions against person
  8. }

借助于组合注解,我们可以进一步简化代码:

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.PARAMETER)
  3. @AggregateWith(PersonAggregator.class)
  4. public @interface CsvToPerson {
  5. }
  1. @ParameterizedTest
  2. @CsvSource({
  3. "Jane, Doe, F, 1990-05-20",
  4. "John, Doe, M, 1990-10-22"
  5. })
  6. void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
  7. // perform assertions against person
  8. }

自定义显示名字

参数化测试生成的test,JUnit Jupiter给定了默认名字,我们可以通过name属性进行自定义。

示例:

  1. @DisplayName("Display name of container")
  2. @ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
  3. @CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
  4. void testWithCustomDisplayNames(String fruit, int rank) {
  5. }

结果:

  1. Display name of container ✔
  2. ├─ 1 ==> the rank of 'apple' is 1
  3. ├─ 2 ==> the rank of 'banana' is 2
  4. └─ 3 ==> the rank of 'lemon, lime' is 3

注意如果要显示'apple',需要使用两层''apple'',因为name是MessageFormat。

占位符说明如下:

image-20210714155314425

小结

本文介绍了JUnit5参数化测试的7种方式,分别是@ValueSource,Null and Empty Sources,@EnumSource@MethodSource@CsvSource@CsvFileSource@ArgumentsSource,比较偏向于Java语法,符合JUnit单元测试框架的特征。另外还介绍了JUnit Jupiter的参数类型转换和参数聚合。最后,如果想要自定义参数化测试的名字,可以使用name属性实现。

最后感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走!

软件测试面试文档

我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。
 

在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/68494
推荐阅读
相关标签
  

闽ICP备14008679号