Junit5参数化测试 - 自定义Json文件源

news/2024/5/19 4:24:02 标签: json, junit5

JUnit5支持参数化测试,即提供多组参数将同一个测试方法运行多次(每次执行前后均会触发@BeforeEach, @AfterEach),
即通过@ParameterizedTest配合参数来源注解使用:

  • @ValueSource(strings = {“1”, “2”})
  • @EnumSource(EnumClass.class)
  • @NullSource, @EmptySource, @NullAndEmptySource
  • @CsvSource({“1,2”, “3,4”})
  • @CsvFileSource(resources = “/demo.csv”, files = “src/test/resources/demo.csv”)
  • @MethodSource(“方法名”)
  • @ArgumentsSource(MyArgumentsProvider.class)
  • 参数转换@ConvertWith(ToStringArgumentConverter.class), @JavaTimeConversionPattern(“dd.MM.yyyy”)
  • 参数聚合ArgumentsAccessor, @AggregateWith(PersonAggregator.class)

简单示例

@ValueSource(strings = {"1", "2"})
@ParameterizedTest
//使用@ParameterizedTest就不要再同时使用@Test了,
//否则报错:ParameterResolutionException: No ParameterResolver registered
void parameterizedTest(String arg) {
    log.info("ParameterizedTest: {}", arg);
}

自定义@JsonFileSource

在编写测试用例的过程中,例如mockMvc工具需要Json字符串参数,
通常我们会构造完参数对象后再通过Json工具序列化成字符串,
又或者直接定义Json字符串(痛苦的转义😭),如:param="{\“key\”:\“value\”}",
以上2种方式都不是很方便,
现在有了Junit5参数化测试,我们可以自定义Json参数来源,思路就是:

  • 自定义json文件(支持多个json文件)用于存放参数
  • 读取json文件的指定jsonPath并提取对应的json字符串
  • jsonPath对应的json内容可以Array、Object,若为Array则对应多个参数(测试多次)

@JsonFileSource 具体实现如下:

import org.apiguardian.api.API;
import org.junit.jupiter.params.provider.ArgumentsSource;

import java.lang.annotation.*;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

/**
 * Junit5参数化测试 - Json文件来源注解<br/>
 * 注:需配合@ParameterizedTest + @JsonFileSource注解使用
 *
 * @author luohq
 * @version 1.0.0
 * @date 2022-01-04
 */
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = EXPERIMENTAL, since = "5.0")
@ArgumentsSource(JsonArgumentsProvider.class)
public @interface JsonFileSource {

    /**
     * 参数对象对应的jsonPath(默认读取root对象)<br/>
     * jsonKey表达式示例如下:
     * <ul>
     *     <li>root对象:$</li>
     *     <li>json对象 or json数组:$.dict.addDict</li>
     *     <li>json数组中的第0个对象:$.dict.addDict[0]</li>
     * </ul>
     */
    String jsonKey() default "$";

    /**
     * json参数来源文件(*.json)(默认测试资源路径下jsonSource.json)
     */
    String resource() default "classpath:jsonSource.json";

    /**
     * json参数需要被转换的Java对象类型(默认String类型)<br/>
     * 注:复杂类型(如嵌套泛型)并未实际测试,<br/>
     * 如有特殊类型(如日期等)可默认转换成String参数后再由自定义Json工具进行反序列化
     */
    Class typeClass() default String.class;
}

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.neusoft.oscoe.osmium.core.exception.bussiness.BusinessException;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONArray;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.support.AnnotationConsumer;
import org.springframework.util.ResourceUtils;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Junit5参数化测试 - Json文件来源 - 解析器<br/>
 * 注:需配合@ParameterizedTest + @JsonFileSource注解使用
 *
 * @author luohq
 * @date 2022-01-04
 * @version 1.0.0
 */
@Slf4j
class JsonArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<JsonFileSource> {
    /**
     * 标注在测试方法上的@JsonFileSource注解
     */
    private JsonFileSource annotation;

    @Override
    public void accept(JsonFileSource annotation) {
        this.annotation = annotation;
    }

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return this.parseJsonParams()
                .stream()
                .map(Arguments::of);
    }

    /**
     * 解析@JsonFileSource.resource指定的Json文件,并提取Json参数列表
     *
     * @return Java参数列表
     */
    private List<Object> parseJsonParams() {
        try {
            //读取json文件
            DocumentContext ctx = JsonPath.parse(ResourceUtils.getFile(this.annotation.resource()));
            //String jsonStr = ctx.read(this.annotation.jsonKey(), new TypeRef<String>() {});
            //解析jsonKey对应的json对象
            Object jsonEle = ctx.read(this.annotation.jsonKey());
            if (null == jsonEle) {
                return Collections.emptyList();
            }

            //转换jsonArray为对象列表
            if (jsonEle instanceof JSONArray) {
                JSONArray jsonArray = (JSONArray) jsonEle;
                AtomicInteger atomIndex = new AtomicInteger(0);
                return jsonArray.stream()
                        .map(jsonItem -> this.convertSingleJsonParam(jsonItem, ctx, atomIndex.getAndIncrement()))
                        .collect(Collectors.toList());
            }

            //转换单个json对象为对象列表
            return Arrays.asList(this.convertSingleJsonParam(jsonEle, ctx, null));
        } catch (Throwable ex) {
            throw new BusinessException("convert jsonFileSource exception!", ex);
        }

    }

    /**
     * 解析单个Json对象为Java参数(支持解析为String和具体Java对象)
     *
     * @param jsonEle json对象
     * @param ctx     json上下文
     * @param index   json数组中当前jsonEle的索引
     * @return Java参数对象
     */
    private Object convertSingleJsonParam(Object jsonEle, DocumentContext ctx, Integer index) {
        //转换json对象为Java字符串
        if (this.annotation.typeClass().equals(String.class)) {
            String jsonObjStr = JsonPath.parse(jsonEle).jsonString();
            log.debug("jsonObjStr: {}", jsonObjStr);
            return jsonObjStr;
        }

        //转换json对象为Java对象(特殊处理jsonArray索引读取)
        String jsonKeyWithIndex = null != index ? String.format("%s[%d]", this.annotation.jsonKey(), index) : this.annotation.jsonKey();
        Object jsonObj = ctx.read(jsonKeyWithIndex, this.annotation.typeClass());
        log.debug("jsonObj: {}", jsonObj);
        return jsonObj;
    }

}

@JsonFileSource使用示例

(1)首先在src/test/resources自定义json参数文件,
例如jsonSource/biz.json
如果服务比较多,也可以定义多个**.json文件,然后通过@JsonFileSource.resource来指定对应的json文件。
在这里插入图片描述

(2)然后在biz.json文件中填充json参数内容,

{
  "addBiz": [
    {
      "bizCode": "biz1",
      "bizDesc": "biz2 desc"
    },
    {
      "bizCode": "biz2",
      "bizDesc": "biz2 desc"
    }
  ],
  "updateBiz": {
    "id": "111",
    "bizCode": "biz2",
    "bizDesc": "biz2 desc"
  }
}

(3)实现测试用例代码

public class BizControllerTest extends BaseTest {
    ...
    
    @ParameterizedTest
    @JsonFileSource(jsonKey = "$.addBiz", resource = "classpath:jsonSource/biz.json")
    void addBiz(String bizJson) throws Exception {
        this.mockMvc.perform(post(API_BASE_PATH)
                //application/json请求体
                .contentType(MediaType.APPLICATION_JSON)
                .characterEncoding(UTF_8)
                //具体请求Json参数
                .content(bizJson))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                //判断响应体code为成功
                .andExpect(jsonPath(JSON_PATH_CODE).value(SystemCodeEnum.SYS_200.getCode()));
    }

    @ParameterizedTest
    @JsonFileSource(jsonKey = "$.updateBiz", resource = resource = "classpath:jsonSource/biz.json")
    void updateBiz(String bizJson) throws Exception {
        this.mockMvc.perform(put(API_BASE_PATH)
                //application/json请求体
                .contentType(MediaType.APPLICATION_JSON)
                .characterEncoding(UTF_8)
                //具体请求Json参数
                .content(bizJson))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
                //判断响应体respCode为成功
                .andExpect(jsonPath(JSON_PATH_CODE).value(SystemCodeEnum.SYS_200.getCode()));
    }
}

接下来便可正常执行测试用例,
最终可以发现addBiz被执行2次,updateBiz被执行1次。


http://www.niftyadmin.cn/n/1151891.html

相关文章

form提交数据处理但不刷新整个页面方法

在form的submit事件中,提交数据成功就会刷新整个页面;而需求中一般是提交数据就只需要局部刷新列表之类的. form在没有action跳转的条件下 方法一: 给form表单的submit函数&#xff0c;在最后加一句 return false; 方法二: 把提交按钮单独放在表单之外,实现数据的提交 转载于:h…

Linux挂载命令mount用法及参数详解

导读mount是Linux下的一个命令&#xff0c;它可以将分区挂接到Linux的一个文件夹下&#xff0c;从而将分区和该目录联系起来&#xff0c;因此我们只要访问这个文件夹&#xff0c;就相当于访问该分区了。挂接命令(mount)首先&#xff0c;介绍一下挂接(mount)命令的使用方法&…

SpringBoot Web Java8日期处理

目录一、输出结果&#xff08;Java8日期类型转换成字符串&#xff09;二、接收参数&#xff08;字符串转换Java8日期类型&#xff09;一、输出结果&#xff08;Java8日期类型转换成字符串&#xff09; 方式1&#xff1a;可在对象属性上单独添加com.fasterxml.jackson.annotati…

Exchange 2003 限制用户向外网发送邮件

在企业系统中&#xff0c;邮件系统起着举足轻重的作用。同时为了符合企业的安全性策略&#xff0c;在Exchange 2003 中&#xff0c;常常需要限制某个用户或组向外网发送邮件&#xff0c;只允许此邮件在内部收发。下面我们以实验的方式来分析在Exchange 2003 中如何限制某个用户…

boost编译随笔

boost下载地址 编译 生成bjam.exe 1.下载boost源码&#xff0c;可以直接使用上面给出的1.60.0版本 2.解压下载到的boost文件&#xff0c;例如解压到 x:\boost_1_60_0 3.使用VisuaStudio编译。打开菜单找到Visual Studio工具&#xff0c;打开Developer Commander&#xff0c;例如…

Camunda入门(一) - 选型及核心概念

目录1. 关于BPM选型 - 推荐Camunda2. Camunda核心概念Process DefinitionProcess DeploymentProcess EngineProcess ApplicationProcess VariablesProcess、Process Instance、Task3. Camunda核心架构Process EngineCamunda Platform集群模型1. 关于BPM选型 - 推荐Camunda 如下…

jenkins(二十二):Jenkins配置钉钉通知

Jenkins配置钉钉通知 https://github.com/jenkinsci/dingding-notifications-plugin https://blog.csdn.net/weixin_34358092/article/details/85089707 建群-配置机器人-取得讨论组token 我的token&#xff0c;就别用我的了吧&#xff01; https://oapi.dingtalk.com/robot/se…

Camunda入门(二) - 启动Camunda管理平台

目录1. 安装Camunda Platform社区版、Camunda Modeler2. SpringBoot集成Camunda Platform Webapps2.1 集成Mysql启动Camunda管理平台&#xff0c;即提供Web管理界面&#xff0c;管理界面主要功能包括&#xff1a; Cockpit - 管理流程process及流程实例process instancesTaskli…