作为新一代的测试框架,Junit5中有很多大家喜欢的测试方案,个人认为最突出的就是能够进行参数化的测试(Parameterized Tests)

简介

通常,会遇到这样的情况,同一个测试案例,改变的只是测试时候输入的参数不同。按照之前的做法,可能会是通过每个输入参数都写一个测试,或者将测试参数封装到集合中循环遍历执行测试。在新版的Junit5中,已经提供了一种更加优雅的方式来进行。

该特性允许我们:该特性可以让我们运行单个测试多次,且使得每次运行仅仅是参数不同而已。

安装依赖

为了使用 JUnit 5 的参数化测试(parameterized tests)。需要在Junit Platform的基础上,导入而外的 junit-jupiter-params 架包。

maven:

<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-params</artifactId><version>5.4.2</version><scope>test</scope>
</dependency>

Gradle:

testCompile("org.junit.jupiter:junit-jupiter-params:5.4.2")

简单的案例

比如,需要测试一个函数是判断输入值否为基数。

public class Numbers {public static boolean isOdd(int number) {return number % 2 != 0;}
}

通过Parameterized Test,则可以写成如下的形式:

@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOdd_ShouldReturnTrueForOddNumbers(int number) {assertTrue(Numbers.isOdd(number));
}

JUnit5 将会执行上面的测试6次,每次都会分配来之*@ValueSource*中不同的int参数.

如何定义不同参数的来源

上面简单的展示了如何通过不同的参数来运行同一个测试。但是很多时候并不仅仅是Int类型

简单值 Simple Value

@ValueSource 可以往测试方法中传递一个数据或者迭代器。可支持的简单参数如下:

  • short (with the shorts attribute)
  • byte (with the bytes attribute)
  • int (with the ints attribute)
  • long (with the longs attribute)
  • float (with the floats attribute)
  • double (with the doubles attribute)
  • char (with the chars attribute)
  • java.lang.String (with the strings attribute)
  • java.lang.Class (with the classes attribute)

值得注意的是,@ValueSource不允许传入Null值和Empty值。从JUnit 5.4开始,我们可以使用@NullSource、@EmptySource 和 @NullAndEmptySource 注解可以分别将单个null值、单个Empty和 Null和Empty 传递给参数化测试方法。

枚举类 Enum

为了运行将一个枚举类的所有的值传入到测试中,可以使用 @EnumSource注解。比如使用枚举类Month

@ParameterizedTest
@EnumSource(Month.class) // passing all 12 months
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {int monthNumber = month.getValue();assertTrue(monthNumber >= 1 && monthNumber <= 12);
}

或者通过names可以过滤掉一些某些枚举类

@ParameterizedTest
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {final boolean isALeapYear = false;assertEquals(30, month.length(isALeapYear));
}

通常的情况下,会被认为names是对匹配的这些名字的枚举类进行操作,但是通过mode=EXCLUDE属性可以设置为取反。

@ParameterizedTest
@EnumSource(value = Month.class,names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"},mode = EnumSource.Mode.EXCLUDE)
void exceptFourMonths_OthersAre31DaysLong(Month month) {final boolean isALeapYear = false;assertEquals(31, month.length(isALeapYear));
}

另外,也可以通过在names属性上增加正则表达式来操作这些迭代的字符串。

@ParameterizedTest
@EnumSource(value = Month.class, names = ".+BER", mode = EnumSource.Mode.MATCH_ANY)
void fourMonths_AreEndingWithBer(Month month) {EnumSet<Month> months =EnumSet.of(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER, Month.DECEMBER);assertTrue(months.contains(month));
}

CSV(最常用)

很多时候,需要同时传入参数预期结果,来验证测试逻辑。比如, 需要去测试toUpperCase()方法(能够将预期的String字符串转换成预期的大写字母字符串)。

@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {String actualValue = input.toUpperCase();assertEquals(expected, actualValue);
}

其中 @CsvSource 注解接受一个以逗号分隔的数组,并且每个数组项都对应于CSV文件中的一行。该注解包含了一个 delimiter 属性,可以用来定义分割符(默认是逗号)。

CSV Files

同前面的CSV一样,只是把参数写到具体的CSV文件存储起来。通过@CsvFileSource注解说明文件路径。

@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile(String input, String expected) {String actualValue = input.toUpperCase();assertEquals(expected, actualValue);
}

通常情况下,@CsvFileSource注解回去解析每一行,但有些时候,第一行可能会是列名,所以在上面的方法中加上了numLinesToSkip 属性来跳过第一行。

方法 Method

通过@MethodSource注解可以传递一些复杂的迭代对象到测试中。

@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {assertEquals(expected, Strings.isBlank(input));
}

其中@MethodSource 注解需要匹配现有存在方法,通常会在同测试类中查询该方法,如果不在同测试类文件下,则需要加上方法名的全限定名。比如下面例子

class StringsUnitTest {@ParameterizedTest@MethodSource("com.baeldung.parameterized.StringParams#blankStrings")void isBlank_ShouldReturnTrueForNullOrBlankStringsExternalSource(String input) {assertTrue(Strings.isBlank(input));}
}public class StringParams {static Stream<String> blankStrings() {return Stream.of(null, "", "  ");}
}

自定义参数提供器 Custom Argument Provider

通过实现ArgumentsProvider接口可以使用一些更加高级的方式去传递参数。比如

class BlankStringsArgumentsProvider implements ArgumentsProvider {@Overridepublic Stream<? extends Arguments> provideArguments(ExtensionContext context) {return Stream.of(Arguments.of((String) null), Arguments.of(""), Arguments.of("   ") );}
}

然后在实际的测试类中引用上面的自定义参数提供器。

@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {assertTrue(Strings.isBlank(input));
}

如何自定义注解

前面的都是Junit 参数化解析框架提供的注解,也可以自定义一些参数注解,以一种更加优美的方式来实现参数化得测试。比如,想实现一个从静态变量里面加载测试参数的注解。类似于如下的代码。

static Stream<Arguments> arguments = Stream.of(Arguments.of(null, true), // null strings should be considered blankArguments.of("", true),Arguments.of("  ", true),Arguments.of("not blank", false)
);@ParameterizedTest
@VariableSource("arguments")
void isBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource(String input, boolean expected) {assertEquals(expected, Strings.isBlank(input));
}

JUnit5 提供了如下的两个基类来帮助我们实现。一个用来实现注解细节,一个用来提供测试参数

  • AnnotationConsumer 基类提供注解细节
  • ArgumentsProvider 基类提供测试的参数

实际的实现如下:

class VariableArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<VariableSource> {private String variableName;@Overridepublic Stream<? extends Arguments> provideArguments(ExtensionContext context) {return context.getTestClass().map(this::getField).map(this::getValue).orElseThrow(() -> new IllegalArgumentException("Failed to load test arguments"));}@Overridepublic void accept(VariableSource variableSource) {variableName = variableSource.value();}private Field getField(Class<?> clazz) {try {return clazz.getDeclaredField(variableName);} catch (Exception e) {return null;}}@SuppressWarnings("unchecked")private Stream<Arguments> getValue(Field field) {Object value = null;try {value = field.get(null);} catch (Exception ignored) {}return value == null ? null : (Stream<Arguments>) value;}
}

参数的类型转换

隐式转换

假设通过@CsvSource 注解来重写了前面@EmumTests 测试。在@CSVSource中通过传入字符串,而不是枚举类

@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Pssing strings
void someMonths_Are30DaysLongCsv(Month month) {final boolean isALeapYear = false;assertEquals(30, month.length(isALeapYear));
}

按理来说是应该失败的,但是实际运行你会发现,它能够正常的运行。

因为Junit5默认会对字符串进行隐式的转换。String默认可以转换成如下的几种类型

  • UUID
  • Locale
  • LocalDate, LocalTime, LocalDateTime, Year, Month, etc.
  • File and Path
  • URL and URI
  • Enum subclasses

显式转换

有些时候,需要提供一种自定义的显式参数类型转换器。例如,想将 yyyy/mm/dd 格式的字符串数据转换成LocalDate实例。

第一步是实现 ArgumentConverter接口

class SlashyDateConverter implements ArgumentConverter {@Overridepublic Object convert(Object source, ParameterContext context)throws ArgumentConversionException {if (!(source instanceof String)) {throw new IllegalArgumentException("The argument should be a string: " + source);}try {String[] parts = ((String) source).split("/");int year = Integer.parseInt(parts[0]);int month = Integer.parseInt(parts[1]);int day = Integer.parseInt(parts[2]);return LocalDate.of(year, month, day);} catch (Exception e) {throw new IllegalArgumentException("Failed to convert", e);}}
}

然后通过 @ConvertWith 注解来引用指定的转换器。

@ParameterizedTest
@CsvSource({"2018/12/25,2018", "2019/02/11,2019"})
void getYear_ShouldWorkAsExpected(@ConvertWith(SlashyDateConverter.class) LocalDate date, int expected) {assertEquals(expected, date.getYear());
}

参数存取器

通常情况下,一个测试参数,会对应一个形参参数。但是当通过一个参数源传递多个参数的时候,则有些时候就会显得很大和混乱。这时候,可以通过一个参数存取器 ArgumentsAccessor来聚合这些参数,然后在使用的时候,根据索引来获得。比如,想测试下面的Person类中的 fullName方法

class Person {String firstName;String middleName;String lastName;// constructorpublic String fullName() {if (middleName == null || middleName.trim().isEmpty()) {return String.format("%s %s", firstName, lastName);}return String.format("%s %s %s", firstName, middleName, lastName);}
}

如果想测试fullName方法,则需要传入 firstName, middleName, lastName, 和 the expected fullName.。我们不通过定义不同的测试形参参数,而是通过 ArgumentsAccessor来解析这些测试参数。

@ParameterizedTest
@CsvSource({"Isaac,,Newton,Isaac Newton", "Charles,Robert,Darwin,Charles Robert Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(ArgumentsAccessor argumentsAccessor) {String firstName = argumentsAccessor.getString(0);String middleName = (String) argumentsAccessor.get(1);String lastName = argumentsAccessor.get(2, String.class);String expectedFullName = argumentsAccessor.getString(3);Person person = new Person(firstName, middleName, lastName);assertEquals(expectedFullName, person.fullName());
}

(讲道理,没有看出太大的优势)好处就是将所有的参数都聚合存储在一起,并且通过下面定义的几个方法来解析。

  • getString(index) 直接通过索引解析了具体的值,成字符串。(返回类型就是String)
  • get(index) 简单通过索引元素解析成 Object对象
  • get(index, type) 将指定的索引元素解析成指定的类型对象 type

参数聚合器

使用前面的参数存取器 ArgumentsAccessor可能会使得代码缺少可读性和复用性。可以提通过自定义一个aggregator来实现。

首先就是实现 ArgumentsAggregator接口

class PersonAggregator implements ArgumentsAggregator {@Overridepublic Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)throws ArgumentsAggregationException {return new Person(accessor.getString(1), accessor.getString(2), accessor.getString(3));}
}

然后通过指定 @AggregateWith 注解来引用

@ParameterizedTest
@CsvSource({"Isaac Newton,Isaac,,Newton", "Charles Robert Darwin,Charles,Robert,Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(String expectedFullName,@AggregateWith(PersonAggregator.class) Person person) {assertEquals(expectedFullName, person.fullName());
}

上面的 PersonAggregator用例中,聚合了最后的3个参数,并且实例化出了一个Person对象。

自定义显式名称 Customizing Display Names

默认情况下,测试运行之后显式的测试名如下

├─ someMonths_Are30DaysLongCsv(Month)
│     │  ├─ [1] APRIL
│     │  ├─ [2] JUNE
│     │  ├─ [3] SEPTEMBER
│     │  └─ [4] NOVEMBER

但是我们可以通过 @ParameterizedTest 注解中的 name 属性来自定义显示名称。例如

@ParameterizedTest(name = "{index} {0} is 30 days long")
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {final boolean isALeapYear = false;assertEquals(30, month.length(isALeapYear));
}

显示的’ April is 30 days long’ 可能更加的表意。

在自定义显示名下,可以使用下面的几个占位符。

  • {index} 表示调用索引,从1开始,然后2,3等
  • {arguments} 表示完整的参数列表,以逗号分隔。
  • **{0}, {1}, …****.*表示单个参数名称。

总结

Junit5 会越来越流行,上面的相关源代码请参考 tutorials/testing-modules/junit5-annotations at master · eugenp/tutorials。

参考文档


  1. https://www.baeldung.com/parameterized-tests-junit-5

Junit5中的参数化测试(Parameterized Tests)指南相关推荐

  1. JUnit5学习之七:参数化测试(Parameterized Tests)进阶

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 关于<JUnit5学习>系列 <JU ...

  2. JUnit5学习之六:参数化测试(Parameterized Tests)基础

    | :-- | :-- | :-- | | 项目主页 | https://github.com/zq2599/blog_demos | 该项目在GitHub上的主页 | | git仓库地址(https ...

  3. java参数化测试除法_TestNG - 参数化测试( Parameterized Test)

    TestNG - 参数化测试( Parameterized Test) TestNG中另一个有趣的功能是parametric testing . 在大多数情况下,您会遇到业务逻辑需要大量不同测试的情况 ...

  4. 软件测试——JUnit中的参数化测试

    2019独角兽企业重金招聘Python工程师标准>>> 参数化测试用于当需要使用多组不同的测试数据测试同一个方法的时候. 使用参数化测试的要点: ① 为该测试方法专门生成一个新的类: ...

  5. 浅谈Junit4和TestNG中的参数化测试

    最近在看Junit4的相关知识,由于本身做的是自动化方面的测试,所以工作上着重于应用TestNG.恰好遇到了一个将case进行参数化的需求,故在此记录Junit4和TestNG在参数化方面的区别. 一 ...

  6. 什么是软件测试中的探索性测试(完整指南)(二)

    目录 Session-based Exploratory Testing基于会话的探索性测试 Pair Based Exploratory Testing基于结对的探索性测试 Exploratory ...

  7. Java单元测试--参数化测试Parameterized的使用示例介绍

    在做单元测试时,可能同一个方法需要传很多个不同的参数进行测试,但是写一个测试参数写一个测试方法会比较冗余,那么有什么办法可以将一组参数进行测试吗?答案是有的. 使用 Parameterized,在测试 ...

  8. junit 参数化测试用例_JUnit:在参数化测试中命名单个测试用例

    junit 参数化测试用例 几年前,我写了有关JUnit参数化测试的文章 . 我不喜欢它们的一件事是JUnit使用数字命名了单个测试用例,因此,如果它们失败,您将不知道是哪个测试参数导致了失败. 以下 ...

  9. JUnit:在参数化测试中命名单个测试用例

    几年前,我写了有关JUnit参数化测试的文章 . 我不喜欢它们的一件事是JUnit使用数字命名了单个测试用例,因此,如果它们失败,您将不知道是哪个测试参数导致了失败. 以下Eclipse屏幕快照将向您 ...

最新文章

  1. 关于双目立体视觉的三大基本算法及发展现状的总结
  2. UVA11646 Athletics Track(计算几何、弧长公式)
  3. 八百客CRM:微信提现收费,仅仅用户钱包受了伤?
  4. java求100以内的a2 b2=c2,Java语言程序设计Ⅱ-中国大学mooc-试题题目及答案
  5. 猜1-10的数字python脚本
  6. CSS hack技巧大全 案例演示
  7. android倒计时动画特效,Android仿活动时分秒倒计时效果
  8. 本周任务asp.net 1.1老系统移植升级到asp.net 2.0,又是一个浩大的工程啊?
  9. JavaScript:JavaScript语法的详细介绍
  10. 不会SQL注入,连漫画都看不懂了
  11. (Quicker)懒人福利:鼠标快捷操作工具箱
  12. 【优化算法】人工蜂鸟优化算法(AHA)【含Matlab源码 1470期】
  13. 计算机网络原理之运输层
  14. 阿里云国际版云服务器Linux和Windows操作系统的链路测试工具-Unirech
  15. VTP技术及相关配置
  16. HTML制作简单课程表
  17. 如何为豆瓣FM写一个chrome的歌词插件
  18. Paravirtualization
  19. 房屋出租系统(第一版)
  20. python2和python3 with open as f写中文乱码

热门文章

  1. 苹果手机怎么编辑word文档_永久免费PDF转word软件,PDF转换王,图片转Word文档,手机照片转可编辑文字...
  2. 2023 年云计算趋势
  3. python tkinter button 透明图片_Tkinter-按钮图像透明背景
  4. playwrite 基础操作启动浏览器
  5. 新概念新技术收集系列(1)
  6. 《NFL橄榄球》:克利夫兰布朗·橄榄1号位
  7. 《卫报》分阶儿童阅读好书单 5-7
  8. 景观平面图转鸟瞰图_2020年东南大学风景园林硕士研究生入学初试真题解析——山地茶园景观规划与设计...
  9. python编写一计票程序,键盘输入候选人姓名(输入“#”结束),使用字典存储并统计出候选人得票数。python实现分段函数。
  10. 移动 短信群发软件开发完成。