参数化集成测试

news2024/9/24 5:22:40

我们在开发项目的过程中遇到了复杂的业务需求,测试同学没有办法帮我们覆盖每一个场景;或者是我们自己在做代码功能升级、技改,而不是业务需求的时候,可能没有测试资源帮我们做测试,那这个时候就需要依靠自己的单元测试来保证产品的质量。

我们的工程一般分为接口层,业务层,仓储层;那每一个模块都需要我们用单元测试来覆盖。

仓储层:这一层,我们一般会连接到真实的数据库,完成仓储层的CRUD,我们可以连到开发库或者测试库,但是仅仅是单元测试就需要对我们资源占用,成本有点高,所以h2基于内存的数据库就很好的帮我们解决这些问题。

业务层:业务层的逻辑比较复杂,我们可以启动整个服务帮助测试,也可以使用mock来覆盖每一个分支,因为用mock的话不需要启动服务,专注我们的业务流程,更快也更方便。

接口层:一般接口层我们会用集成测试的较多,启动整个服务端到端的流程捋下来,采用BDD的思想,给什么入参,期望什么结果,写测试用例的时候只是专注于入参出参就行,测试代码不用做任何改变。

首先junit4和junit5都支持参数化的测试,但我用下来感觉到内置的这些功能不能够满足我的需求,所以我一般会自定义数据类型。

下面以一个controller接口为例完成集成测试:

采用springboot+mybatisplus完成基础功能,代码忽略,只贴一下controller和配置文件

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/query")
    public UserDO query(String username) {
        LambdaQueryWrapper<UserDO> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UserDO::getUsername, username);
        return userService.getOne(queryWrapper);
    }
}
spring:
  application:
    name: fiat-exchange
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/maple?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
    username: root
    password: ''

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

首先我们上面说集成测试需要启动整个服务,DB采用h2的基于内存的数据库,同时需要初始化库与表

spring:
  profiles:
    active: test
  application:
    name: integration-testing-local-test
  datasource:
    # 测试用的内存数据库,模拟MSQL
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test;mode=mysql
    username: root
    password: test
  sql:
    init:
      schema-locations: classpath:schema.sql
      data-locations: classpath:data.sql
      mode: always

schema.sql

DROP TABLE `user` IF EXISTS;
CREATE TABLE `user` (
    `username` varchar(64) COMMENT 'username',
    `password` varchar(64) COMMENT 'password'
) ENGINE=InnoDB DEFAULT CHARSET = utf8 COMMENT='user';

data.sql

INSERT INTO `user` (`username`, `password`) VALUES ('maple', '123456');
INSERT INTO `user` (`username`, `password`) VALUES ('cc', '654321');

IntegrationTestingApplicationTests

package com.maple.integration.testing;

import com.maple.integration.testing.controller.UserController;
import com.maple.integration.testing.entity.UserDO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(classes = IntegrationTestingApplication.class)
class IntegrationTestingApplicationTests {

    @Autowired
    private UserController userController;

    @Test
    void contextLoads() {
        UserDO userDO = userController.query("maple");
        assertThat(userDO.getPassword()).isEqualTo("123456");
    }
}

工程结构

 单元测试可以跑通了,但是如果要加测试用例的话就需要再加代码加用例,不符合我们的要求,我们要求能有一个地方放入参出参就行,下面我们改造下。

1、BaseTestData

基类,任何测试bean都需要集成它

@Data
public abstract class BaseTestData {

    private String testCaseName;

    private Object[] expectedResult;
}

2、 UserDTOTestData

@Data
@EqualsAndHashCode(callSuper = true)
public class UserDTOTestData extends BaseTestData {
    private String username;
}

3、JsonFileSource

@ParameterizedTest 使用junit5的参数化测试的主键,他内置了一些功能注解,比如:MethodSource、EnumSource、CsvFileSource等,我们参考内置的来自定义JsonFileSource,可以测试单个用例,也可以扫描文件路径测试批量用例

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(
        status = API.Status.EXPERIMENTAL,
        since = "5.0"
)
@ArgumentsSource(JsonFileArgumentsProvider.class)
public @interface JsonFileSource {

    /**
     * 文件路径:controller.userController.query/
     * @return
     */
    String directoryPath() default "";

    /**
     * 具体的文件路径:/controller.userController.query/validCase_QueryUser.json
     * @return
     */
    String[] resources() default "";

    String encoding() default "UTF-8";

    Class<?> typeClass();
}

4、JsonFileArgumentsProvider

package com.beet.fiat.config;

import com.alibaba.fastjson.JSON;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
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.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Stream;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * @author maple.wang
 * @date 2022/11/17 16:24
 */
public class JsonFileArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<JsonFileSource> {

    private final BiFunction<Class<?>, String, InputStream> inputStreamProvider;

    private String[] resources;

    private String directoryPath;

    private String encoding;

    private Class<?> typeClass;

    private static final ClassLoader CLASS_LOADER = JsonFileArgumentsProvider.class.getClassLoader();

    public JsonFileArgumentsProvider() {
        this(Class::getResourceAsStream);
    }

    public JsonFileArgumentsProvider(BiFunction<Class<?>, String, InputStream> inputStreamProvider) {
        this.inputStreamProvider = inputStreamProvider;
    }

    @Override
    public void accept(JsonFileSource jsonFileSource) {
        this.directoryPath = jsonFileSource.directoryPath();
        this.resources = jsonFileSource.resources();
        this.encoding = jsonFileSource.encoding();
        this.typeClass = jsonFileSource.typeClass();
    }

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {

        String displayName = extensionContext.getDisplayName();
        System.out.println(displayName);

        if(StringUtils.isNotBlank(directoryPath)){
            List<String> resourcesFromDirectoryPath = getResources(directoryPath);
            String[] resourcesArrayFromDirectoryPath = Optional.of(resourcesFromDirectoryPath).orElse(null).toArray(String[]::new);
            if(Objects.nonNull(resourcesArrayFromDirectoryPath) && resourcesArrayFromDirectoryPath.length > 0){
                resources = ArrayUtils.addAll(resourcesArrayFromDirectoryPath, resources);
            }
        }

        return Arrays.stream(resources)
                .filter(StringUtils::isNotBlank)
                .map(resource -> openInputStream(extensionContext, resource))
                .map(this::createObjectFromJson)
                .map(str -> JSON.parseObject(str, typeClass))
                .map(Arguments::of);
    }

    private List<String> getResources(String directoryPath) throws IOException{
        List<String> testFileNames;
        try (InputStream directoryStream = CLASS_LOADER.getResourceAsStream(directoryPath)) {
            if (directoryStream == null) {
                return List.of();
            }

            testFileNames = IOUtils.readLines(directoryStream, UTF_8);
        }

        // for each file found, parse into TestData
        List<String> testCases = new ArrayList<>();
        for (String fileName : testFileNames) {
            Path path = Paths.get(directoryPath, fileName);
            testCases.add("/" + path);
        }
        return testCases;
    }

    private String createObjectFromJson(InputStream inputStream) {
        try {
            return StreamUtils.copyToString(inputStream, Charset.forName(encoding));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private InputStream openInputStream(ExtensionContext context, String resource) {
        Preconditions.notBlank(resource, "Classpath resource [" + resource + "] must not be null or blank");
        Class<?> testClass = context.getRequiredTestClass();
        return Preconditions.notNull(inputStreamProvider.apply(testClass, resource),
                () -> "Classpath resource [" + resource + "] does not exist");
    }
}

IntegrationTestingApplicationTests 就变为了

    @ParameterizedTest
    @JsonFileSource(resources = "/controller.userController.query/validCase_QueryUser.json", typeClass = UserDTOTestData.class)
    @DisplayName("query user")
    void queryUser(UserDTOTestData testData) {
        UserDO userDO = userController.query(testData.getUsername());
        assertThat(userDO.getPassword()).isEqualTo(testData.getExpectedResult()[0]);
    }

    @ParameterizedTest
    @JsonFileSource(directoryPath = "controller.userController.query/", typeClass = UserDTOTestData.class)
    @DisplayName("query user")
    void queryUsers(UserDTOTestData testData) {
        UserDO userDO = userController.query(testData.getUsername());
        assertThat(userDO.getPassword()).isEqualTo(testData.getExpectedResult()[0]);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/79070.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构之树相关概念的知识铺垫

文章目录前言1.树的相关介绍2. 树的表示3.二叉树概念及结构4.二叉树的性质5.二叉树相关概念练习6.总结前言 之前对数组结构中线性结构进行了相关的介绍&#xff0c;本文将开始对非线性结构进行相关的介绍&#xff0c;首先介绍的是树&#xff0c;会围绕树的相关概念进行初步的简…

记住这三个方法,让你的钱越花越多

小狗钱钱 关于作者 本书作者博多•舍费尔&#xff0c;是德国著名的投资 家、企业家、演说家以及畅销书作家。他 人生中曾出现过严重的财务危机&#xff0c;但他凭 着自己的努力&#xff0c;重新获得了财务自甶。这 段经历让他产生了和更多人分享理财知识 的想法&#xff0c;《…

玩以太坊链上项目的必备技能(类型-映射类型-Solidity之旅四)

映射&#xff08;Mapping&#xff09; 说到映射&#xff08;Mapping&#xff09;&#xff0c;有过其它编程语言经验的您&#xff0c;对这是再熟悉不过了。Solidity 中的映射&#xff08;Mapping&#xff09; 与Java的Map、Go里的Map以及javascript的JSON等众多编程语言中的Map…

考虑柔性负荷的综合能源系统低碳经济优化调度【考虑碳交易机制】(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

【大数据入门核心技术-ElasticSearch】(一)ElasticSearch介绍

目录 一、什么是Elasticsearch? 二、核心概念 1.倒排索引&#xff08;Inverted Index&#xff09; 2.节点 & 集群&#xff08;Node & Cluster&#xff09; 3.索引&#xff08;Index&#xff09; 4.文档&#xff08;Document&#xff09; 5.类型&#xff08;type…

游戏开发50课 性能优化8

4.2 渲染状态优化 4.2.1 状态缓存 在引擎侧&#xff0c;可以使用状态缓存减少渲染管线的切换。伪代码&#xff1a; class RenderStateCache { public:void InitRenderStates();{for (RenderStateType tRenderStateType.begin; t<RenderStateType.end; i){_renderStateCac…

[附源码]计算机毕业设计基于微信小程序的网络办公系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

微服务框架 SpringCloud微服务架构 微服务保护 31 限流规则 31.3 流控模式【链路】

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 微服务保护 文章目录微服务框架微服务保护31 限流规则31.3 流控模式【链路】31.3.1 流控模式 - 链路31.3.2 举个栗子31.3.3 总结31 限流规则…

Vite + Vue3 + Electron 创建打包桌面程序、实现进程通信

目录 1. Electron 介绍 2. 使用 Vite 构建 Electron 项目 2.1 创建 Vite 应用&#xff0c;安装 Electron 依赖 2.2 在 vite.config.ts 中&#xff0c;配置 Electron 入口文件 2.3 编写 electron / index.ts 2.3.1 app、BrowserWindow 2.3.2 使用 win.loadURL 加载窗口 /…

Autosar MCAL-GTM之TOM

文章目录前言定时器输出模块&#xff08;TOM&#xff09;TOM产生PWM的原理TOM中断中断使能中断模式中断映射CFG中TOM通道配置TOM Channel EnableTomChDisableOnTgcTriggerTOM Channel EnableTOM Channel OutputTomChOutputDisableOnTgcTrigTomChannelOutputControlTomChannelOu…

【项目_05】tabcontrol的搭建及回显、使用keep-alive缓存页面、移动端适配 | 基于Vue3全家桶

&#x1f4ad;&#x1f4ad; ✨&#xff1a;tabcontrol的搭建及回显、使用keep-alive缓存页面   &#x1f49f;&#xff1a;东非不开森的主页   &#x1f49c;: 总不能还没努力就向生活妥协吧&#x1f49c;&#x1f49c;   &#x1f338;: 如有错误或不足之处&#xff0c;希…

【双向ConvLSTM Network:遥感融合】

D2TNet: A ConvLSTM Network With Dual-Direction Transfer for Pan-Sharpening &#xff08;D2TNet: 双向传输的卷积长短期记忆递归神经网络用于泛锐化&#xff09; 本文提出了一种高效的具有双向传输的卷积长短期记忆递归神经网络&#xff08;convolutional long short-ter…

Java Agent 探针技术

Java 中的 Agent 技术可以让我们无侵入性的去进行代理&#xff0c;最常用于程序调试、热部署、性能诊断分析等场景&#xff0c;现如今比较火热的分布式链路追踪项目Skywalking&#xff0c;就是通过探针技术去捕获日志&#xff0c;将数据上报OAP观察分析平台。 Java Agent 技术简…

Socket编程实现TCP、UDP样例

文章目录一.Socket简介二.Socket实现TCPTCP通信简介使用对象及方法简介代码实现服务端代码客户端代码三.Socket实现UDPUDP通信简介UDP程序的使用步骤代码实现服务端代码客户端代码一.Socket简介 socket套接字是通信的基石&#xff0c;是支持TCP/IP协议的路通信的基本操作单元.…

私企招聘:思特威社会招聘

关于我们 思特威&#xff08;上海&#xff09;电子科技股份有限公司 SmartSens Technology &#xff08;股票简称&#xff1a;思特威&#xff0c;股票代码&#xff1a;688213&#xff09;是一家从事CMOS图像传感器芯片产品研发、设计和销售的高新技术企业&#xff0c;总部设立…

pads logic 生成参考编号带分隔符以及不统计不贴元器件的BOM

1.查看BOM报告 &#xff0c;这里可以通过下面的方法 Step1:点击文件 Step2: 勾选材料清单&#xff0c;然后点击设置 Step3:在弹出的如下窗口&#xff0c;选择剪切板视图 &#xff0c;然后选择全选,然后复制&#xff0c;将数据粘贴到excel表格中即可。 2. 参考编号分隔符 …

以前不知道字节面试难在哪,现在体验到了,被虐的很惨

…(这里省略一些不清楚、不知道、忘记了之类的词藻&#xff0c;保留一丝尊严。) 接下来是关于redis哲学三连“是什么?为什么?怎么用?” 我把肚子里仅有的关于Redis的一滴墨水挤成了三滴&#xff0c;仍然没有给出他想要的。继续追问Redis的数据存储方式&#xff0c;操作方法…

微服务框架 SpringCloud微服务架构 微服务保护 30 初识Sentinel 30.1 雪崩问题及解决方案

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 微服务保护 文章目录微服务框架微服务保护30 初识Sentinel30.1 雪崩问题及解决方案30.1.1 雪崩问题30.1.2 总结30 初识Sentinel 30.1 雪崩问…

类别不平衡Class-imbalance解决方法

类别不平衡是指分类任务中不同类别的训练样例数目差别很大的情况。 1、扩大数据集 2、欠采样 欠采样&#xff08;under-sampling&#xff09;&#xff1a;对大类的数据样本进行采样来减少该类数据样本的个数&#xff0c;使其与其他类数目接近&#xff0c;然后再进行学习。 随…

第十四届蓝桥杯集训——JavaC组第九篇——位运算符

第十四届蓝桥杯集训——JavaC组第九篇——位运算符 目录 第十四届蓝桥杯集训——JavaC组第九篇——位运算符 值交换 异或值交换 其它位移符号&#xff1a; 这个稍微难度大一些&#xff0c;基础的有【&与、|或、^异或、<<左位移、>>右位移】。 我们一个一个…