欢送拜访我的GitHub
https://github.com/zq2599/blog_demos
内容:所有原创文章分类汇总及配套源码,波及Java、Docker、Kubernetes、DevOPS等;
对于《JUnit5学习》系列
《JUnit5学习》系列旨在通过实战晋升SpringBoot环境下的单元测试技能,一共八篇文章,链接如下:
- 基本操作
- Assumptions类
- Assertions类
- 按条件执行
- 标签(Tag)和自定义注解
- 参数化测试(Parameterized Tests)根底
- 参数化测试(Parameterized Tests)进阶
- 综合进阶(终篇)
本篇概览
- 本文是《JUnit5学习》系列的第七篇,前文咱们对JUnit5的参数化测试(Parameterized Tests)有了根本理解,能够应用各种数据源管制测试方法屡次执行,明天要在此基础上更加深刻,把握参数化测试的一些高级性能,解决理论问题;
- 本文由以下章节组成:
- 自定义数据源
- 参数转换
- 多字段聚合
- 多字段转对象
- 测试执行名称自定义
源码下载
- 如果您不想编码,能够在GitHub下载所有源码,地址和链接信息如下表所示:
名称 | 链接 | 备注 |
---|---|---|
我的项目主页 | https://github.com/zq2599/blo... | 该我的项目在GitHub上的主页 |
git仓库地址(https) | https://github.com/zq2599/blo... | 该我的项目源码的仓库地址,https协定 |
git仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该我的项目源码的仓库地址,ssh协定 |
- 这个git我的项目中有多个文件夹,本章的利用在<font color="blue">junitpractice</font>文件夹下,如下图红框所示:
- <font color="blue">junitpractice</font>是父子构造的工程,本篇的代码在<font color="red">parameterized</font>子工程中,如下图:
自定义数据源
- 前文应用了很多种数据源,如果您对它们的各种限度不称心,想要做更彻底的个性化定制,能够开发<font color="blue">ArgumentsProvider</font>接口的实现类,并应用<font color="blue">@ArgumentsSource</font>指定;
- 举个例子,先开发ArgumentsProvider的实现类<font color="blue">MyArgumentsProvider.java</font>:
package com.bolingcavalry.parameterized.service.impl;import org.junit.jupiter.api.extension.ExtensionContext;import org.junit.jupiter.params.provider.Arguments;import org.junit.jupiter.params.provider.ArgumentsProvider;import java.util.stream.Stream;public class MyArgumentsProvider implements ArgumentsProvider { @Override public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception { return Stream.of("apple4", "banana4").map(Arguments::of); }}
- 再给测试方法增加<font color="blue">@ArgumentsSource</font>,并指定<font color="red">MyArgumentsProvider</font>:
@Order(15) @DisplayName("ArgumentsProvider接口的实现类提供的数据作为入参") @ParameterizedTest @ArgumentsSource(MyArgumentsProvider.class) void argumentsSourceTest(String candidate) { log.info("argumentsSourceTest [{}]", candidate); }
- 执行后果如下:
参数转换
- 参数化测试的数据源和测试方法入参的数据类型必须要保持一致吗?其实JUnit5并没有严格要求,而事实上JUnit5是能够做一些主动或手动的类型转换的;
- 如下代码,数据源是int型数组,但测试方法的入参却是double:
@Order(16) @DisplayName("int型主动转为double型入参") @ParameterizedTest @ValueSource(ints = { 1,2,3 }) void argumentConversionTest(double candidate) { log.info("argumentConversionTest [{}]", candidate); }
- 执行后果如下,可见int型被转为double型传给测试方法(Widening Conversion):
- 还能够指定转换器,以转换器的逻辑进行转换,上面这个例子就是将字符串转为LocalDate类型,要害是<font color="blue">@JavaTimeConversionPattern</font>:
@Order(17) @DisplayName("string型,指定转换器,转为LocalDate型入参") @ParameterizedTest @ValueSource(strings = { "01.01.2017", "31.12.2017" }) void argumentConversionWithConverterTest( @JavaTimeConversionPattern("dd.MM.yyyy") LocalDate candidate) { log.info("argumentConversionWithConverterTest [{}]", candidate); }
- 执行后果如下:
字段聚合(Argument Aggregation)
- 来思考一个问题:如果数据源的每条记录有多个字段,测试方法如何能力应用这些字段呢?
- 回顾方才的@CsvSource示例,如下图,可见测试方法用两个入参对应CSV每条记录的两个字段,如下所示:
- 上述形式应答大量字段还能够,但如果CSV每条记录有很多字段,那测试方法岂不是要定义大量入参?这显然不适合,此时能够思考JUnit5提供的字段聚合性能<font color="blue">(Argument Aggregation)</font>,也就是将CSV每条记录的所有字段都放入一个<font color="blue">ArgumentsAccessor</font>类型的对象中,测试方法只有申明ArgumentsAccessor类型作为入参,就能在办法外部获得CSV记录的所有字段,成果如下图,可见CSV字段实际上是保留在ArgumentsAccessor实例外部的一个Object数组中:
- 如下图,为了不便从ArgumentsAccessor实例获取数据,ArgumentsAccessor提供了获取各种类型的办法,您能够按理论状况选用:
- 上面的示例代码中,CSV数据源的每条记录有三个字段,而测试方法只有一个入参,类型是ArgumentsAccessor,在测试方法外部,能够用ArgumentsAccessor的getString、get等办法获取CSV记录的不同字段,例如<font color="blue">arguments.getString(0)</font>就是获取第一个字段,失去的后果是字符串类型,而<font color="blue">arguments.get(2, Types.class)</font>的意思是获取第二个字段,并且转成了Type.class类型:
@Order(18) @DisplayName("CsvSource的多个字段聚合到ArgumentsAccessor实例") @ParameterizedTest @CsvSource({ "Jane1, Doe1, BIG", "John1, Doe1, SMALL" }) void argumentsAccessorTest(ArgumentsAccessor arguments) { Person person = new Person(); person.setFirstName(arguments.getString(0)); person.setLastName(arguments.getString(1)); person.setType(arguments.get(2, Types.class)); log.info("argumentsAccessorTest [{}]", person); }
- 上述代码执行后果如下图,可见通过<font color="blue">ArgumentsAccessor</font>可能获得CSV数据的所有字段:
更优雅的聚合
- 后面的聚合解决了获取CSV数据多个字段的问题,但仍然有瑕疵:从ArgumentsAccessor获取数据生成Person实例的代码写在了测试方法中,如下图红框所示,测试方法中应该只有单元测试的逻辑,而创立Person实例的代码放在这里显然并不适合:
- 针对下面的问题,JUnit5也给出了计划:通过注解的形式,指定一个从ArgumentsAccessor到Person的转换器,示例如下,可见测试方法的入参有个注解<font color="blue">@AggregateWith</font>,其值PersonAggregator.class就是从ArgumentsAccessor到Person的转换器,而入参曾经从后面的<font color="blue">ArgumentsAccessor</font>变成了<font color="red">Person</font>:
@Order(19) @DisplayName("CsvSource的多个字段,通过指定聚合类转为Person实例") @ParameterizedTest @CsvSource({ "Jane2, Doe2, SMALL", "John2, Doe2, UNKNOWN" }) void customAggregatorTest(@AggregateWith(PersonAggregator.class) Person person) { log.info("customAggregatorTest [{}]", person); }
- <font color="blue">PersonAggregator</font>是转换器类,须要实现ArgumentsAggregator接口,具体的实现代码很简略,也就是从ArgumentsAccessor示例获取字段创立Person对象的操作:
package com.bolingcavalry.parameterized.service.impl;import org.junit.jupiter.api.extension.ParameterContext;import org.junit.jupiter.params.aggregator.ArgumentsAccessor;import org.junit.jupiter.params.aggregator.ArgumentsAggregationException;import org.junit.jupiter.params.aggregator.ArgumentsAggregator;public class PersonAggregator implements ArgumentsAggregator { @Override public Object aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) throws ArgumentsAggregationException { Person person = new Person(); person.setFirstName(arguments.getString(0)); person.setLastName(arguments.getString(1)); person.setType(arguments.get(2, Types.class)); return person; }}
- 上述测试方法的执行后果如下:
进一步简化
- 回顾一下方才用注解指定转换器的代码,如下图红框所示,您是否回忆起JUnit5反对自定义注解这一茬,咱们来把红框局部的代码再简化一下:
- 新建注解类<font color="blue">CsvToPerson.java</font>,代码如下,非常简单,就是把上图红框中的<font color="blue">@AggregateWith(PersonAggregator.class)</font>搬过去了:
package com.bolingcavalry.parameterized.service.impl;import org.junit.jupiter.params.aggregator.AggregateWith;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.PARAMETER)@AggregateWith(PersonAggregator.class)public @interface CsvToPerson {}
- 再来看看上图红框中的代码能够简化成什么样子,间接用@CsvToPerson就能够将ArgumentsAccessor转为Person对象了:
@Order(20) @DisplayName("CsvSource的多个字段,通过指定聚合类转为Person实例(自定义注解)") @ParameterizedTest @CsvSource({ "Jane3, Doe3, BIG", "John3, Doe3, UNKNOWN" }) void customAggregatorAnnotationTest(@CsvToPerson Person person) { log.info("customAggregatorAnnotationTest [{}]", person); }
- 执行后果如下,可见和<font color="blue">@AggregateWith(PersonAggregator.class)</font>成果统一:
测试执行名称自定义
- 文章最初,咱们来看个轻松的知识点吧,如下图红框所示,每次执行测试方法,IDEA都会展现这次执行的序号和参数值:
- 其实上述红框中的内容格局也能够定制,格局模板就是<font color="blue">@ParameterizedTest</font>的<font color="red">name</font>属性,批改后的测试方法残缺代码如下,可见这里改成了中文形容信息:
@Order(21) @DisplayName("CSV格局多条记录入参(自定义展现名称)") @ParameterizedTest(name = "序号 [{index}],fruit参数 [{0}],rank参数 [{1}]") @CsvSource({ "apple3, 31", "banana3, 32", "'lemon3, lime3', 0x3A" }) void csvSourceWithCustomDisplayNameTest(String fruit, int rank) { log.info("csvSourceWithCustomDisplayNameTest, fruit [{}], rank [{}]", fruit, rank); }
- 执行后果如下:
- 至此,JUnit5的参数化测试(Parameterized)相干的知识点曾经学习和实战实现了,把握了这么弱小的参数输出技术,咱们的单元测试的代码覆盖率和场景范畴又能够进一步晋升了;
你不孤独,欣宸原创一路相伴
- Java系列
- Spring系列
- Docker系列
- kubernetes系列
- 数据库+中间件系列
- DevOps系列
欢送关注公众号:程序员欣宸
微信搜寻「程序员欣宸」,我是欣宸,期待与您一起畅游Java世界...
https://github.com/zq2599/blog_demos