Spring Boot 3.x Web单元测试最佳实践

news2024/11/28 18:42:24

上一篇:Spring Boot 3.x Rest API统一异常处理最佳实践

下一篇:Spring Boot 3.x Filter实战:记录请求日志

Spring Boot为我们提供了非常便捷的webRest API单元测试的API,这种开发能力也是小伙伴必须要掌握的。如何对数据库、中间件服务以及远程调用在开发环境不可使用的情况进行Rest API功能测试,本教程将为小伙伴揭秘。如果觉得对你有帮助,记得点赞收藏,关注小卷,后续更精彩!

在这里插入图片描述

文章目录

    • 依赖与配置
    • 创建单元测试
    • mock mvc测试
      • @MockBean注解
      • 执行请求发送
      • 假定条件下的测试
      • perform结果断言
      • 异常结果断言
      • 完善异常测试捕获环节
    • @AutoConfigureMockMvc测试方式
    • 客户端测试方式
    • 总结

依赖与配置

接下来我们通过web单元测试来对之前开发好的API进行测试,先看下build.gradle中跟单元测试相关的配置:

dependencies {
    ...
    // 为单元测试环境引入和启用lombok编译功能
    testAnnotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    ...
}

tasks.named('test') {
    useJUnitPlatform()
}

创建单元测试

创建单元测试的方式,在API接口上使用组合键Alt+Enter(这里对idea使用eclipse风格的快捷键),比如:

点击创建Test,选择默认的junit5单元测试框架,并选择要测试的方法,点创建:

mock mvc测试

看下面的单元测试结构:

package com.juan.demo.api;

import ...

import static org.junit.jupiter.api.Assertions.*;

@WebMvcTest(CartAPI.class)
class CartAPITest {

    @Resource
    private MockMvc mvc;

    @Test
    void addCartItem() {
        assertNotNull(mvc);
    }
}

我们在类的头部使用@WebMvcTest注解,表明要启动一个web测试的应用模拟环境,也就是说并不会启动一个spring boot应用的真正上下文环境,对于需要注入的组件需要额外mock出来,直接运行以上单元测试用例,得到这样的错误:

在这里插入图片描述

...
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.juan.demo.service.CartService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@jakarta.annotation.Resource(shareable=true, lookup="", name="", description="", authenticationType=CONTAINER, type=java.lang.Object.class, mappedName="")}
...

@MockBean注解

为此,我们可以通过@MockBean注解来注入一个模拟实现的CartService组件:

...
class CartAPITest {
    ...
    
    @MockBean
    private CartService cartService;
    
    ...
}

这样再启动单元测试,执行结果绿条,我们就模拟出了要测试的API的web环境,并对其依赖的后台组件进行模拟注入,以确保模拟上下文可以正常进行依赖注入。但是需要注意:对哪些API接口启动模拟测试环境是由@WebMvcTest注解控制的,默认是所有的API,也可以指定(这里我们指定API接口即可),比如这里我们指定的是CartAPI,而如果我们指定成@WebMvcTest(HelloAPI.class),则很显然,环境依赖更简单(无需用@@MockBean注解注入任何模拟依赖)

动一动小手

小伙伴们可以按照上述注意事项,自行做实验来验证结论。

执行请求发送

环境启动ok,有了模拟mvc的MockMvc实例之后,就可以在启动的web模拟环境中借助MockMvc实例来发送和测试API请求了。看下测试用例:

package com.juan.demo.api;

import ...

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(CartAPI.class)
class CartAPITest {

    @Resource
    private MockMvc mvc;

    @MockBean
    private CartService cartService;

    @SneakyThrows
    @Test
    void addCartItem() {
        assertNotNull(mvc);

        this.mvc.perform(post("/personal/cart")
                        .accept(MediaType.APPLICATION_JSON_VALUE)
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .content("{\"p\":2,\"q\":4}"))
                .andExpect(status().isOk());
    }
}

从测试API支持的连缀写法,以及静态方法的包裹调用,一目了然,这里我们发送了一个路径为/personal/cart的添加购物车API请求,发送和接收的都是json格式,发送的数据硬编码的json字符串,期望返回的http状态码为200。注意,这里perform方法会抛出异常,为了简化处理我们在测试用例上加了@SneakyThrows注解。

启动单元测试,测试ok,从控制台输出的日志可以看出,CartServiceaddCartItem方法返回的结果为空:

在这里插入图片描述

为什么会这样呢?明明我们在CartService的实现中对这种输入情况会抛出异常。

在这里插入图片描述

要注意,我们这里用@MockBean注解注入的其实是一个CartService的模拟代理实现,我们需要告诉模拟的代理对象,什么情况下返回什么样的结果。

假定条件下的测试

为此,我们给定这样的条件:当接收的购物车添加项,商品id为5且数量为4时,返回的购物车列表中有一个商品id为5的条目,且总的添加数量为20

// 静态导入
import static org.mockito.BDDMockito.given;

...
@Test
void addCartItem() {
    ...
    given(this.cartService.addCartItem(new CartItemDTO(5L, 4)))
            .willReturn(List.of(new CartItemDTO(5L, 20)));
    // 执行mockMvc调用代码省略
    ...
}

基于这样的预设,现在我们调整下执行单元测试发送的json内容,为了方便设值,这里我们构造一个CartItemDTO对象,并使用注入的ObjectMapper实例对其进行json序列化处理后,传给测试API的content(...)方法调用:

package com.juan.demo.api;
import ...
...
class CartAPITest {
    ...
    @Resource
    private ObjectMapper objectMapper;
    ...
    void addCartItem() {
        ...
        String jsonStr = objectMapper.writeValueAsString(new CartItemDTO(5L, 4));
        this.mvc.perform(...
                        .content(jsonStr))
                ...
    }
}

因为这里我们发送的内容和模拟CartService实现中给定的条件完全符合,因此测试得到这样的输出:

在这里插入图片描述

我们也可以对假定的条件放宽,比如只需要匹配商品id,而不用关心数量,则可以对不关心的字段值使用any()来替换:

// 注意静态导入
import static org.mockito.ArgumentMatchers.any;
...

given(this.cartService.addCartItem(new CartItemDTO(5L, any())))
        .willReturn(...);

针对执行结果的断言,我们可以做进一步的完善:

// 为方便静态导入,使用*通配符
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

...
this.mvc.perform(...)
        ...
        .andExpect(cookie().exists("cart_data"))
        .andExpect(jsonPath("status").value(0));

这里我们期望给客户端的数据中包含了写入的特定key的cookie信息,同时对响应的json内容进行检查。

MockMvcResultMatchers类型的匹配器中提供了返回各种匹配器的静态方法以便对各种类型响应数据进行匹配,比如这里的cookie()调用返回的CookieResultMatchers可以对cookie信息进行匹配。而jsonPath()很显然我们可以通过json path表达式的方式来抠取json中嵌套的节点内容进行断言。

perform结果断言

除此之外,我们还可以对MockMvc实例的perform方法调用返回的结果进行相关信息的获取并断言:

// 注意静态导入
import static org.assertj.core.api.Assertions.assertThat;

...
MvcResult result = this.mvc.perform(...)
        ...
    .andReturn();
// 从响应对象中获取cookie信息
String cartData = result.getResponse().getCookie("cart_data").getValue();
// 对cookie字符串解码
String decode = URLDecoder.decode(cartData);
// 反序列化
List<CartItemDTO> cartItems = objectMapper.readValue(decode, new TypeReference<>() {});
// 断言写入cookie的结果
assertThat(cartItems.size()).isEqualTo(1);
assertThat(cartItems.get(0).getProductId()).isEqualTo(5);
assertThat(cartItems.get(0).getQuantity()).isEqualTo(20);

这里在对json字符串反序列化为对象时,我们对TypeReference后接的泛型类型采用JDK9开始支持的钻石语法,让代码看着更简洁。

另外这里的断言,我们采用了org.assertj.core.api.Assertions包下提供的assertThat方法进行优雅的断言。

异常结果断言

这里我们还可以进行异常情况的测试,比如我们给定:当添加id为2的商品时,会抛出已经下架的异常,为此我们又编写了一个测试用例:

// 通配的静态导入
import static org.junit.jupiter.api.Assertions.*;


@SneakyThrows
@Test
void addCartItemWithException() {

    given(this.cartService.addCartItem(new CartItemDTO(2L, any())))
            .willThrow(new BusinessException("该商品已经下架,请另拍"));

    String jsonStr = objectMapper.writeValueAsString(new CartItemDTO(2L, 4));

    mvc.perform(post("/personal/cart")
                    .accept(MediaType.APPLICATION_JSON_VALUE)
                    .contentType(MediaType.APPLICATION_JSON_VALUE)
                    .content(jsonStr))
            .andExpect(status().is5xxServerError())
            .andExpect(result -> assertTrue(result.getResolvedException() instanceof BusinessException))
            .andExpect(result -> assertEquals("该商品已经下架,请另拍", result.getResolvedException().getMessage()))
            .andExpect(jsonPath("status").value(1))
            .andExpect(jsonPath("msg").value("该商品已经下架,请另拍"));
}

在该用例中,我们假定了添加购物车失败的场景,断言的结果我们判断了返回的http状态、异常的类型、异常信息以及返回的json内容。但很遗憾的是,测试代码在执行perform时抛出了我们自定义的BusinessException异常,后续的断言不会再被执行。

在这里插入图片描述

究其原因,官网文档说的很清楚:

Test with a mock environment

在这里插入图片描述

意思很清楚,这里mockMvc的测试方式仅仅是mock了Spring MVC层的运行环境,而非整个web容器处理请求的全流程,包括Web容器自身的Filter、Servlet组件以及控制器后面比较偏后的Spring Web模块处理流程(比如Spring Boot通过DefaultErrorAttributes组件来处理全局错误信息的流程)。这种仅仅启用局部组件的模拟运行环境运行比较快,仅关心控制器层的测试,符合单元测试关注点分离的测试理念。

完善异常测试捕获环节

对于这里异常没有被测试框架捕获处理转成可被断言的错误信息,是因为这种处理流程上我们对抛出的异常没有做必要的处理。解决办法有两种:

  1. BusinessException类上加要映射的http状态

    ...
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public class BusinessException extends RuntimeException { ... }
    

    显然,这种绑定死状态码的方式违背了我们当初的设计的初衷。

  2. ExceptionHandler方式

    这种方式来捕获处理异常,我们在之前的全局异常处理中并未采用,原因是这是属于spring mvc的处理方式,而我们采用的是直接扩展spring boot的处理方式。但在mock mvc的测试流程中,我们需要把这一环补上,以完成整个测试流程。为此,我们在test包下创建一个异常处理类来做这件事:

    package com.juan.demo.web.testing.support;
    
    import ...
    
    @Slf4j
    @RestControllerAdvice
    public class TestingExceptionHandler {
    
        @ExceptionHandler(value = BusinessException.class)
        public Response<?> handleBusinessException(BusinessException ex) {
            // 抛出和打印异常堆栈
            log.error(ex.getMessage(), ex);
            // 设置http异常响应状态
            RequestContextUtil.getResponse().setStatus(ex.getStatus().value());
            return Response.fail(ex);
        }
    
    }
    

    这里我们只是对测试用例中抛出BusinessException的情况进行了捕获处理,而对于其他的异常类型也只要照葫芦画瓢的加进来即可。需要注意,这里要往http响应对象中设置http异常状态码,这里我们用之前封装的工具类。

    因为最终流程还要走到RestBodyAdvicebeforeBodyWrite拦截处理方法中,我们需要额外判断第一个参数body如果已经是Response类型则,直接记录日志并返回,增加的判断处理逻辑:

    public Object beforeBodyWrite(Object body, ...) {
    
        ...
    
        Type type = ...
        ...
        if (type == String.class) {
            ...
        } else if (body instanceof Response<?>) {
            // 这种情况属于在exceptionHandler中进行错误响应对象的包装
            return logResp((Response<?>) body);
        }
        ...
    }
    

最后,再执行测试,ok!

在这里插入图片描述

@AutoConfigureMockMvc测试方式

除了直接使用@WebMvcTest注解,还可以使用@SpringBootTest结合@AutoConfigureMockMvc的方式,后者会在spring boot启动时启用mockMvc的自动配置,来模拟spring mvc的测试环境,并注入MockMvc的bean实例。这种启动方式会对所有API实现中涉及到的依赖完成真实bean组件的注入,看下面的例子:

package com.juan.demo.api;

import ...

@SpringBootTest
// 启用mockMvc自动配置
@AutoConfigureMockMvc
class APITest {

    @Resource
    private ObjectMapper objectMapper;

    @Resource
    private MockMvc mvc;

    @SneakyThrows
    @Test
    void addCartItem() {

        String jsonStr = objectMapper.writeValueAsString(new CartItemDTO(2L, 4));

        mvc.perform(post("/personal/cart")
                        .accept(MediaType.APPLICATION_JSON_VALUE)
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .content(jsonStr))
                .andExpect(status().is5xxServerError())
                .andExpect(result -> assertTrue(result.getResolvedException() instanceof BusinessException))
                .andExpect(result -> assertEquals("该商品已经下架,无法添加到购物车", result.getResolvedException().getMessage()));
    }
}

该测试用例,实际调用的是我们的CartServiceImpl的实现,在实现逻辑中,我们加了硬编码的判断:如果是id为2的商品,就抛出已下架的异常。

在这里插入图片描述

当然,必要的时候(比如某些后台服务无法访问时),我们也可以采用@MockBean模拟注入的方式来替换掉CartService的真实实现。

APITest类中引入下面代码:

@MockBean
private CartService cartService;

再运行测试看看,实际CartService的代理实现并未返回数据。这种情况读者可以自行测试验证。

客户端测试方式

除了前面介绍的mock mvc的方式以及结合模拟代理组件的给定条件结果进行web层的测试外,我们还可以使用@SpringBootTest注解来启动web容器并使用测试客户端组件来完成web单元测试,看下面的例子:

package com.juan.demo.api;

import ...

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class APIClientTest {

    @Test
    void addCartItem(@Autowired TestRestTemplate restTemplate) {
        assertNotNull(restTemplate);
    }
}

这里我们指定了启动的web容器运行在一个随机的端口,启动测试环境后,我们可以获得方法注入的TestRestTemplate对象进行客户端连接服务器的测试。TestRestTemplate的内部其实依赖了一个对测试友好且配置完好的RestTemplate对象。以下是用它来发送请求和进行断言的逻辑:

...
void addCartItem(@Autowired TestRestTemplate restTemplate) {
    ...
    ResponseEntity<Response<?>> re = restTemplate.exchange("/personal/cart", HttpMethod.POST,
            new HttpEntity<>(new CartItemDTO(2L, 4), new HttpHeaders()), new ParameterizedTypeReference<>() {});
    assertThat(re.getStatusCode().is5xxServerError()).isTrue();
    assertThat(re.getBody().getStatus()).isEqualTo(1);
    assertThat(re.getBody().getMsg()).isEqualTo("该商品已经下架,无法添加到购物车");
}

测试结果:

在这里插入图片描述

总结

通过对之前开发好的添加购物车api的各种形式的web单元测试,相信小伙伴们对此有很好的掌握了,并能在企业开发中很好的编写web单元测试来保证rest api开发任务的质量以及提高自测的效率。大家加油!

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

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

相关文章

【简历】扬州某一本大学:前端秋招简历指导,面试通过率低

注&#xff1a;为保证用户信息安全&#xff0c;姓名和学校等信息已经进行同层次变更&#xff0c;内容部分细节也进行了部分隐藏 简历说明 这是25届一本前端同学的简历。这是一个老牌一本学校&#xff0c;老牌一本定位求职层次&#xff0c;可以从传统的中厂上升到大厂。学历可以…

Pytorch离线文件的快速下载

一、为什么要使用离线方式安装Pytorch 参考我的博客《直接用文件方式安装Cuda版本的Pytorch》可以方便的安装Cuda版本的Pytorch&#xff0c;比较方便快捷。系统重装后&#xff0c;可以快速的重新搭建系统。 二、如何直接下载Pytorch的离线安装文件whl 可以参考这个博客&#…

基于SpringBoot的桂林二手房交易系统的设计与实现---附源码17680

目录 1 绪论 1.1 选题背景与意义 1.2国内外研究现状 1.3论文结构与章节安排 2系统分析 2.1 可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析 2.3 系统用例分析 2.4 系统流程分析 2.4.1系统开发流程 2.4.2 用户登录流程 2.4.3 系统操作流程 2.4…

java数字产科管理系统源码,产科业务信息系统源码,产科电子病历系统源码,前端框架:Vue、ElementUI 数 据 库:MySQL8.0.36

数字产科管理系统源码&#xff0c;产科业务信息系统源码&#xff0c;产科电子病历系统源码 数字产科管理系统是一套针对孕产妇的基于流程管控的产科业务信息系统。该系统由门诊系统、住院系统、数据统计模块三部分组成。实现孕产妇围产期一待产一住院的持续化、专业化、电子化…

高性能并行计算面试-核心概念-问题理解

目录 1.什么是并行计算&#xff1f;高性能从哪些方面体现&#xff1f; 2.CPU常见的并行技术 3.GPU并行 4.并发与并行 5.常见的并行计算模型 6.如何评估并行程序的性能&#xff1f; 7.描述Am达尔定律和Gustafson定律&#xff0c;并解释它们对并行计算性能的影响 8.并行计…

python使用 数值微分法 求梯度,实现单层线性回归

文章目录 模型构建数据数值微分实现&#xff08;梯度计算&#xff09;模型封装运行测试运行结果 主要介绍 数值微分法 求梯度&#xff0c;以及基于此对参数作随机梯度下降&#xff0c;并封装一个简单的线性回归模型以作调试&#xff0c;最后绘制loss图像。 模型 y X W b y X…

Java:接口interface

文章目录 接口interface好处为什么要用接口 接口案例需求思路代码Student.javaClassManage.javaStudentOperator 接口StudentOperatorImpl1.javaStudentOperatorImpl2.javaTest.java 黑马程序员学习笔记 接口interface 接口中&#xff1a;变量默认为常量&#xff0c;方法默认为…

上门预约o2o系统源码开发及商业模式探索

随着互联网的飞速发展&#xff0c;O2O&#xff08;Online to Offline&#xff09;模式已成为连接线上与线下服务的重要桥梁。上门预约O2O系统作为这一模式下的典型应用&#xff0c;通过整合线上线下资源&#xff0c;为用户提供便捷、高效、个性化的上门服务体验。本文将从商业模…

WebStorm 2024 for Mac/Win:JavaScript开发的高效利器

WebStorm 2024 for Mac/Win是一款专为前端开发者和全栈工程师设计的集成开发环境&#xff08;IDE&#xff09;&#xff0c;由JetBrains公司精心打造。这款软件以其强大的功能和卓越的性能&#xff0c;在JavaScript及相关技术的开发领域中脱颖而出&#xff0c;成为众多开发者的首…

无人机电池的使用寿命!

无人机电池的循环寿命一般在100次到500次之间&#xff0c;具体取决于电池类型、质量和使用条件。高品质电池和正确的使用方式可以延长电池的循环寿命。 避免极端温度 避免在过高或过低的温度下使用无人机电池&#xff0c;以免影响电池性能和寿命。 正确存储 将电池存放在干燥…

C语言——预处理详解(下)

目录 前言 #和## 1.#运算符 2.##运算符 命名约定 #undef 命令行定义 条件编译 1.单分支条件编译 2.多分支条件编译 3.判断是否被定义 4.嵌套指令 头文件的包含 1.头文件被包含的方式 (1)本地文件包含 (2)库文件包含 2.嵌套文件包含 其他预处理指令 1.#error…

如何用Python进行数据可视化、科技图表绘制?

目录 写在前面 推荐图书 推荐理由 写在最后 写在前面 有了它&#xff0c;科技图表绘制、数据可视化真的毫无难度&#xff01; 推荐图书 《Python数据可视化&#xff1a;科技图表绘制》(芯智)【摘要 书评 试读】- 京东图书 图书简介 《Python数据可视化:科技图表绘制》结…

mfc140.dll丢失如何修复,一步步教你如何解决mfc140.dll丢失,让电脑快速恢复正常状态!

mfc140.dll是 Microsoft Foundation Class (MFC) Library 的一部分&#xff0c;它是一个用于开发 Windows 应用程序的 C 库。当系统报告mfc140.dll丢失时&#xff0c;通常意味着某个应用程序需要这个 DLL 文件来运行&#xff0c;但系统中没有找到它。那么mfc140.dll丢失如何修复…

Ubuntu下提升高并发socket最大连接数限制

文章目录 前言1. limits.conf修改2. /etc/pam.d修改3. /etc/sysctl.conf修改4. ulimit设置5.重启系统即可生效参考文档 前言 linux系统默认ulimit为1024个访问 用户最多可开启的程序数目。一般一个端口&#xff08;即一个进程&#xff09;的最高连接为2的16次方65536。 查看全…

TPshop商城的保姆教程(Ubuntu)

1.上传TPSHOP源码 选择适合自己的版本下载 TPshop商城源文件下载链接&#xff1a; 百度网盘 请输入提取码 上传tpshop的源码包到特定目录/var/www/html 切换到/var/www/html 目录下 cd /var/www/html修改HTML目录下所有文件权限 chmod -R 777 * 2.打开网址配置 TPshop安…

如何以编程方式解析 XCResult 包的内容

文章目录 介绍查找 XCResult 包分享 XCResult 包 解析 XCResult 包自动解析 XCResult 包的内容 使用 XCResultKit 解析包的内容初始化库获取调用记录 获取测试信息导出屏幕录制 可运行 Demo初始化 Swift Package编写主文件代码解释运行 Demo 结论 介绍 XCResult 包是一个包含运…

Apache SeaTunnel 2.3.5 Zeta-Server集群环境搭建与使用

作者 | 月影幽篁 在当前数据驱动的业务环境中&#xff0c;快速且高效的数据处理能力至关重要。Apache SeaTunnel以其卓越的性能和灵活性&#xff0c;成为数据工程师和开发者的首选工具之一。本文将介绍如何在集群环境中搭建Apache SeaTunnel 2.3.5版本的 Zeta-Server&#xff…

期权强大优势之一的杠杆是什么?!

今天带你了解期权强大优势之一的杠杆是什么&#xff1f;&#xff01;期权是一种合约&#xff0c;该合约赋予持有人在某一特定日期以固定价格买入或卖出一种资产的权利。 期权杠杆是指使用较少的资金控制相对较大金额的股票或其他资产的能力。 期权提供了买入或卖出标的资产的…

U盘救星在此!年度免费数据恢复软件TOP榜

现在这社会&#xff0c;数字信息太重要了&#xff0c;工作文件、学习笔记&#xff0c;还有那些记录美好时光的照片和视频&#xff0c;要是一不小心丢了&#xff0c;那可真是急死人。不过&#xff0c;幸运的是&#xff0c;现在有数据恢复软件&#xff0c;它们就像是数据的救星&a…

Qt多线程编程-run()方法

本文介绍Qt多线程编程-run()方法。 Qt多线程编程主要有2种方法&#xff0c;前面已经介绍了moveToThread()方法&#xff0c;本文介绍另外一种方法run()方法&#xff0c;并给出一个实例参考。 1.基本原理 run()方法首先需要定义一个基于QThread的派生类&#xff0c;QThread类是…