欢迎访问我的GitHub

https://github.com/zq2599/blog_demos

内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;

关于《JUnit5学习》系列

《JUnit5学习》系列旨在通过实战提升SpringBoot环境下的单元测试技能,一共八篇文章,链接如下:

  1. 基本操作
  2. Assumptions类
  3. Assertions类
  4. 按条件执行
  5. 标签(Tag)和自定义注解
  6. 参数化测试(Parameterized Tests)基础
  7. 参数化测试(Parameterized Tests)进阶
  8. 综合进阶(终篇)

本篇概览

  • 本文是《JUnit5学习》系列的第七篇,前文咱们对JUnit5的参数化测试(Parameterized Tests)有了基本了解,可以使用各种数据源控制测试方法多次执行,今天要在此基础上更加深入,掌握参数化测试的一些高级功能,解决实际问题;
  • 本文由以下章节组成:
  1. 自定义数据源
  2. 参数转换
  3. 多字段聚合
  4. 多字段转对象
  5. 测试执行名称自定义

源码下载

  1. 如果您不想编码,可以在GitHub下载所有源码,地址和链接信息如下表所示:
名称 链接 备注
项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  1. 这个git项目中有多个文件夹,本章的应用在junitpractice文件夹下,如下图红框所示:

  1. junitpractice是父子结构的工程,本篇的代码在parameterized子工程中,如下图:

自定义数据源

  1. 前文使用了很多种数据源,如果您对它们的各种限制不满意,想要做更彻底的个性化定制,可以开发ArgumentsProvider接口的实现类,并使用@ArgumentsSource指定;
  2. 举个例子,先开发ArgumentsProvider的实现类MyArgumentsProvider.java:
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);
}
}
  1. 再给测试方法添加@ArgumentsSource,并指定MyArgumentsProvider:
    @Order(15)
@DisplayName("ArgumentsProvider接口的实现类提供的数据作为入参")
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void argumentsSourceTest(String candidate) {
log.info("argumentsSourceTest [{}]", candidate);
}
  1. 执行结果如下:

参数转换

  1. 参数化测试的数据源和测试方法入参的数据类型必须要保持一致吗?其实JUnit5并没有严格要求,而事实上JUnit5是可以做一些自动或手动的类型转换的;
  2. 如下代码,数据源是int型数组,但测试方法的入参却是double:
    @Order(16)
@DisplayName("int型自动转为double型入参")
@ParameterizedTest
@ValueSource(ints = { 1,2,3 })
void argumentConversionTest(double candidate) {
log.info("argumentConversionTest [{}]", candidate);
}
  1. 执行结果如下,可见int型被转为double型传给测试方法(Widening Conversion):

  1. 还可以指定转换器,以转换器的逻辑进行转换,下面这个例子就是将字符串转为LocalDate类型,关键是@JavaTimeConversionPattern:
    @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);
}
  1. 执行结果如下:

字段聚合(Argument Aggregation)

  1. 来思考一个问题:如果数据源的每条记录有多个字段,测试方法如何才能使用这些字段呢?
  2. 回顾刚才的@CsvSource示例,如下图,可见测试方法用两个入参对应CSV每条记录的两个字段,如下所示:



3. 上述方式应对少量字段还可以,但如果CSV每条记录有很多字段,那测试方法岂不是要定义大量入参?这显然不合适,此时可以考虑JUnit5提供的字段聚合功能(Argument Aggregation),也就是将CSV每条记录的所有字段都放入一个ArgumentsAccessor类型的对象中,测试方法只要声明ArgumentsAccessor类型作为入参,就能在方法内部取得CSV记录的所有字段,效果如下图,可见CSV字段实际上是保存在ArgumentsAccessor实例内部的一个Object数组中:



4. 如下图,为了方便从ArgumentsAccessor实例获取数据,ArgumentsAccessor提供了获取各种类型的方法,您可以按实际情况选用:

  1. 下面的示例代码中,CSV数据源的每条记录有三个字段,而测试方法只有一个入参,类型是ArgumentsAccessor,在测试方法内部,可以用ArgumentsAccessor的getString、get等方法获取CSV记录的不同字段,例如arguments.getString(0)就是获取第一个字段,得到的结果是字符串类型,而arguments.get(2, Types.class)的意思是获取第二个字段,并且转成了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);
}
  1. 上述代码执行结果如下图,可见通过ArgumentsAccessor能够取得CSV数据的所有字段:

更优雅的聚合

  1. 前面的聚合解决了获取CSV数据多个字段的问题,但依然有瑕疵:从ArgumentsAccessor获取数据生成Person实例的代码写在了测试方法中,如下图红框所示,测试方法中应该只有单元测试的逻辑,而创建Person实例的代码放在这里显然并不合适:



2. 针对上面的问题,JUnit5也给出了方案:通过注解的方式,指定一个从ArgumentsAccessor到Person的转换器,示例如下,可见测试方法的入参有个注解@AggregateWith,其值PersonAggregator.class就是从ArgumentsAccessor到Person的转换器,而入参已经从前面的ArgumentsAccessor变成了Person:

    @Order(19)
@DisplayName("CsvSource的多个字段,通过指定聚合类转为Person实例")
@ParameterizedTest
@CsvSource({
"Jane2, Doe2, SMALL",
"John2, Doe2, UNKNOWN"
})
void customAggregatorTest(@AggregateWith(PersonAggregator.class) Person person) {
log.info("customAggregatorTest [{}]", person);
}
  1. PersonAggregator是转换器类,需要实现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;
}
}
  1. 上述测试方法的执行结果如下:

进一步简化

  1. 回顾一下刚才用注解指定转换器的代码,如下图红框所示,您是否回忆起JUnit5支持自定义注解这一茬,咱们来把红框部分的代码再简化一下:



2. 新建注解类CsvToPerson.java,代码如下,非常简单,就是把上图红框中的@AggregateWith(PersonAggregator.class)搬过来了:

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 {
}
  1. 再来看看上图红框中的代码可以简化成什么样子,直接用@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);
}
  1. 执行结果如下,可见和@AggregateWith(PersonAggregator.class)效果一致:

测试执行名称自定义

  1. 文章最后,咱们来看个轻松的知识点吧,如下图红框所示,每次执行测试方法,IDEA都会展示这次执行的序号和参数值:

  1. 其实上述红框中的内容格式也可以定制,格式模板就是@ParameterizedTest的name属性,修改后的测试方法完整代码如下,可见这里改成了中文描述信息:
    @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);
}
  1. 执行结果如下:

  • 至此,JUnit5的参数化测试(Parameterized)相关的知识点已经学习和实战完成了,掌握了这么强大的参数输入技术,咱们的单元测试的代码覆盖率和场景范围又可以进一步提升了;

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

欢迎关注公众号:程序员欣宸

微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界...

https://github.com/zq2599/blog_demos

JUnit5学习之七:参数化测试(Parameterized Tests)进阶的更多相关文章

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

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  2. Junit5中实现参数化测试

    从Junit5开始,对参数化测试支持进行了大幅度的改进和提升.下面我们就一起来详细看看Junit5参数化测试的方法. 部署和依赖 和Junit4相比,Junit5框架更多在向测试平台演进.其核心组成也 ...

  3. JUnit5学习之八:综合进阶(终篇)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  4. JUnit5学习之一:基本操作

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  5. JUnit5学习之二:Assumptions类

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  6. JUnit5学习之三:Assertions类

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  7. JUnit5学习之四:按条件执行

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  8. JUnit5学习之五:标签(Tag)和自定义注解

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  9. JUnit5参数化测试的几种方式

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

随机推荐

  1. linux(8)Linux 查看端口占用情况

    前言 平常使用linux,我们经常需要查看哪个服务占用了哪个端口,接下来就为大家介绍了2种 Linux 查看端口占用情况可以使用 lsof 和 netstat 命令. 1. lsof -i:端口号 用 ...

  2. WPF 之 MultiBinding(多路 Binding)(四)

    一.前言 ​ 有时候 UI 需要显示的信息由不止一个数据来源决定,这时候就需要使用 MultiBinding ,即多路 Binding. ​ MultiBinding 与 Binding 一样均以 B ...

  3. A - 欧拉回路

    欧拉回路是指不令笔离开纸面,可画过图中每条边仅一次,且可以回到起点的一条回路.现给定一个图,问是否存在欧拉回路? Input测试输入包含若干测试用例.每个测试用例的第1行给出两个正整数,分别是节点数N ...

  4. Codeforces Round #625 Div. 2 D E

    D题:https://codeforces.com/contest/1321/problem/D 题意:题目给个有向图,然后给一段序列,我们要沿着这个序列走,问走的过程中当前点到t的最短路会重构多少次 ...

  5. c++ stl nth_element 原理解析

    nth_element是stl中的一个库函数,它会使迭代器nth所指的元素与整个[first,last)完整排序后.同一个位置的元素同值.即找到完整排序时第n位的正确值. 但这个函数与完整排序的区别在 ...

  6. Windows10与虚拟机中CentOS-7.2进行ftp通信

    首先Linux的IP地址可以通过以下命令获取: ifconfig Windows10上面IP地址通过下面命令获取 ipconfig 你首先要保证你的主机和Linux虚拟机是可以ping通的(ping都 ...

  7. trie浅谈

    关于trie ​ 其实字典树和以上两种算法有很大不同,但是hash由于其优秀的应用,导致有些字符串查找用hash也是可行的. ​ 字典树中支持添加,查找,区间查询(可持久化字典树),而且在异或操作上有 ...

  8. 博弈论入门——Nim游戏引入

    说实话,我真的对这个游戏看得是一脸懵逼,因为(我太弱了)我没有明白一些变量的意思,所以一直很懵,现在才明白,这让我明白博弈论(还可以骗钱)博大精深; 以下是我自己思考的过程,也许不严谨,但是最终明白了 ...

  9. 怎样优化SQL

    [原则一:选择需要优化的SQL] 1,选择需要优化的SQL:不是所有的SQL都需要优化,在优化的过程中,首选更需要优化的SQL; 怎么选择?优先选择优化高并发低消耗的SQL: 1,1小时请求1W次,1 ...

  10. 1.ASP.NET Core 管道、中间件、依赖注入

    自定义中间件(基于工厂) 自定义中间件(注入到第三方容器)