乐趣区

关于云计算:JUnit5学习之七参数化测试Parameterized-Tests进阶

欢送拜访我的 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/blo… 该我的项目在 GitHub 上的主页
git 仓库地址(https) https://github.com/zq2599/blo… 该我的项目源码的仓库地址,https 协定
git 仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该我的项目源码的仓库地址,ssh 协定
  1. 这个 git 我的项目中有多个文件夹,本章的利用在 <font color=”blue”>junitpractice</font> 文件夹下,如下图红框所示:

  1. <font color=”blue”>junitpractice</font> 是父子构造的工程,本篇的代码在 <font color=”red”>parameterized</font> 子工程中,如下图:

自定义数据源

  1. 前文应用了很多种数据源,如果您对它们的各种限度不称心,想要做更彻底的个性化定制,能够开发 <font color=”blue”>ArgumentsProvider</font> 接口的实现类,并应用 <font color=”blue”>@ArgumentsSource</font> 指定;
  2. 举个例子,先开发 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);
    }
}
  1. 再给测试方法增加 <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);
    }
  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 类型,要害是 <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);
    }
  1. 执行后果如下:

字段聚合(Argument Aggregation)

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

  1. 上述形式应答大量字段还能够,但如果 CSV 每条记录有很多字段,那测试方法岂不是要定义大量入参?这显然不适合,此时能够思考 JUnit5 提供的字段聚合性能 <font color=”blue”>(Argument Aggregation)</font>,也就是将 CSV 每条记录的所有字段都放入一个 <font color=”blue”>ArgumentsAccessor</font> 类型的对象中,测试方法只有申明 ArgumentsAccessor 类型作为入参,就能在办法外部获得 CSV 记录的所有字段,成果如下图,可见 CSV 字段实际上是保留在 ArgumentsAccessor 实例外部的一个 Object 数组中:

  1. 如下图,为了不便从 ArgumentsAccessor 实例获取数据,ArgumentsAccessor 提供了获取各种类型的办法,您能够按理论状况选用:

  1. 上面的示例代码中,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);
    }
  1. 上述代码执行后果如下图,可见通过 <font color=”blue”>ArgumentsAccessor</font> 可能获得 CSV 数据的所有字段:

更优雅的聚合

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

  1. 针对下面的问题,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);
    }
  1. <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;
    }
}
  1. 上述测试方法的执行后果如下:

进一步简化

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

  1. 新建注解类 <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 {}
  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. 执行后果如下,可见和 <font color=”blue”>@AggregateWith(PersonAggregator.class)</font> 成果统一:

测试执行名称自定义

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

  1. 其实上述红框中的内容格局也能够定制,格局模板就是 <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);
    }
  1. 执行后果如下:

  • 至此,JUnit5 的参数化测试 (Parameterized) 相干的知识点曾经学习和实战实现了,把握了这么弱小的参数输出技术,咱们的单元测试的代码覆盖率和场景范畴又能够进一步晋升了;

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

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

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

微信搜寻「程序员欣宸」,我是欣宸,期待与您一起畅游 Java 世界 …
https://github.com/zq2599/blog_demos

退出移动版