Mock测试你的Spring MVC接口

1. 前言
在Java开发中接触的开发者大多数不太注重对接口的测试,结果在联调对接中出现各种问题。也有的使用Postman等工具进行测试,虽然在使用上没有什么问题,如果接口增加了权限测试起来就比较恶心了。所以建议在单元测试中测试接口,保证在交付前先自测接口的健壮性。今天就来分享一下胖哥在开发中是如何对Spring MVC接口进行测试的。
在开始前请务必确认添加了Spring Boot Test相关的组件,在最新的版本中应该包含以下依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>
本文是在Spring Boot 2.3.4.RELEASE下进行的。
2. 单独测试控制层
如果我们只需要对控制层接口(Controller)进行测试,且该接口不依赖@Service、@Component等注解声明的Spring Bean时,可以借助@WebMvcTest来启用只针对Web控制层的测试,例如
@WebMvcTest
class CustomSpringInjectApplicationTests {
    @Autowired
    MockMvc mockMvc;
    @SneakyThrows
    @Test
    void contextLoads() {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/map"))
                .andExpect(ResultMatcher.matchAll(status().isOk(),
                        content().contentType(MediaType.APPLICATION_JSON),
                        jsonPath("$.test", Is.is("hello"))))
                .andDo(MockMvcResultHandlers.print());
    }
}
这种方式要快的多,它只加载了应用程序的一小部分。但是如果你涉及到服务层这种方式是不凑效的,我们就需要另一种方式了。
3. 整体测试
大多数Spring Boot下的接口测试是整体而又全面的测试,涉及到控制层、服务层、持久层等方方面面,所以需要加载比较完整的Spring Boot上下文。这时我们可以这样做,声明一个抽象的测试基类:
package cn.felord.custom;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
/**
 * 测试基类,
 * @author felord.cn
 */
@SpringBootTest
@AutoConfigureMockMvc
abstract class CustomSpringInjectApplicationTests {
    /**
     * The Mock mvc.
     */
    @Autowired
    MockMvc mockMvc;
    // 其它公共依赖和处理方法
}
只有当
@AutoConfigureMockMvc存在时MockMvc才会被注入Spring IoC。
然后针对具体的控制层进行如下测试代码的编写:
package cn.felord.custom;
import lombok.SneakyThrows;
import org.hamcrest.core.Is;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
 * 测试FooController.
 *
 * @author felord.cn
 */
public class FooTests extends CustomSpringInjectApplicationTests {
    /**
     * /foo/map接口测试.
     */
    @SneakyThrows
    @Test
    void contextLoads() {
        mockMvc.perform(MockMvcRequestBuilders.get("/foo/map"))
                .andExpect(ResultMatcher.matchAll(status().isOk(),
                        content().contentType(MediaType.APPLICATION_JSON),
                        jsonPath("$.test", Is.is("bar"))))
                .andDo(MockMvcResultHandlers.print());
    }
}
4. MockMvc测试
集成测试时,希望能够通过输入URL对Controller进行测试,如果通过启动服务器,建立http client进行测试,这样会使得测试变得很麻烦,比如,启动速度慢,测试验证不方便,依赖网络环境等,为了可以对Controller进行测试就引入了MockMvc。
MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。接下来我们来一步步构造一个测试的模拟请求,假设我们存在一个下面这样的接口:
@RestController
@RequestMapping("/foo")
public class FooController {
    @Autowired
    private MyBean myBean;
    @GetMapping("/user")
    public Map<String, String> bar(@RequestHeader("Api-Version") String apiVersion, User user) {
        Map<String, String> map = new HashMap<>();
        map.put("test", myBean.bar());
        map.put("version", apiVersion);
        map.put("username", user.getName());
        //todo your business
        return map;
    }
}
参数设定为name=felord.cn&age=18,那么对应的HTTP报文是这样的:
GET /foo/user?name=felord.cn&age=18 HTTP/1.1
Host: localhost:8888
Api-Version: v1
可以预见的返回值为:
{
    "test": "bar",
    "version": "v1",
    "username": "felord.cn"
}
事实上对接口的测试可以分为以下几步。
构建请求
构建请求由MockMvcRequestBuilders负责,他提供了请求方法(Method),请求头(Header),请求体(Body),参数(Parameters),会话(Session)等所有请求的属性构建。/foo/user接口的请求可以转换为:
MockMvcRequestBuilders.get("/foo/user")
                .param("name", "felord.cn")
                .param("age", "18")
                .header("Api-Version", "v1")
执行Mock请求
然后由MockMvc执行Mock请求:
mockMvc.perform(MockMvcRequestBuilders.get("/foo/user")
                .param("name", "felord.cn")
                .param("age", "18")
                .header("Api-Version", "v1"))
对结果进行处理
请求结果被封装到ResultActions对象中,它封装了多种让我们对Mock请求结果进行处理的方法。
对结果进行预期期望
ResultActions#andExpect(ResultMatcher matcher)方法负责对响应的结果的进行预期期望,看看是否符合测试的期望值。参数ResultMatcher负责从响应对象中提取我们需要期望的部位进行预期比对。
假如我们期望接口/foo/user返回的是JSON,并且HTTP状态为200,同时响应体包含了version=v1的值,我们应该这么声明:
   ResultMatcher.matchAll(MockMvcResultMatchers.status().isOk(),
                MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON),
                MockMvcResultMatchers.jsonPath("$.version", Is.is("v1")));
JsonPath是一个强大的JSON解析类库,请通过其项目仓库https://github.com/json-path/JsonPath了解。
对响应进行处理
ResultActions#andDo(ResultHandler handler)方法负责对整个请求/响应进行打印或者log输出、流输出,由MockMvcResultHandlers工具类提供这些方法。我们可以通过以上三种途径来查看请求响应的细节。
例如/foo/user接口:
MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /foo/user
       Parameters = {name=[felord.cn], age=[18]}
          Headers = [Api-Version:"v1"]
             Body = null
    Session Attrs = {}
Handler:
             Type = cn.felord.xbean.config.FooController
           Method = cn.felord.xbean.config.FooController#urlEncode(String, Params)
Async:
    Async started = false
     Async result = null
Resolved Exception:
             Type = null
ModelAndView:
        View name = null
             View = null
            Model = null
FlashMap:
       Attributes = null
MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"test":"bar","version":"v1","username":"felord.cn"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
获取返回结果
如果你希望进一步处理响应的结果,也可以通过ResultActions#andReturn()拿到MvcResult 类型的结果进行进一步的处理。
完整的测试过程
通常andExpect是我们必然会选择的,而andDo和andReturn在某些场景下会有用,它们两个是可选的。我们把上面的连在一起。
@Autowired
MockMvc mockMvc;
@SneakyThrows
@Test
void contextLoads() {
     mockMvc.perform(MockMvcRequestBuilders.get("/foo/user")
            .param("name", "felord.cn")
            .param("age", "18")
            .header("Api-Version", "v1"))
            .andExpect(ResultMatcher.matchAll(status().isOk(),
                    content().contentType(MediaType.APPLICATION_JSON),
                    jsonPath("$.version", Is.is("v1"))))
            .andDo(MockMvcResultHandlers.print());
}
这种流式的接口单元测试从语义上看也是比较好理解的,你可以使用各种断言、正例、反例测试你的接口,最终让你的接口更加健壮。
5. 总结
一旦你熟练了这种方式,你编写的接口将更加具有权威性而不会再漏洞百出,甚至有时候你也可以使用Mock来设计接口,使之更加贴合业务。所以CRUD不是完全没有技术含量,高质量高效率的CRUD往往需要这种工程化的单元测试来支撑。好了今天的分享就到这里,我是:码农小胖哥,多多关注,多多支持。
关注公众号:Felordcn  获取更多资讯
Mock测试你的Spring MVC接口的更多相关文章
- 换一种方式编写 Spring MVC 接口
		
1. 前言 通常我们编写 Spring MVC 接口的范式是这样的: @RestController @RequestMapping("/v1/userinfo") public ...
 - Spring MVC接口实例
		
概述 前文记录了MVC模式和Spring MVC的初步认识,现在记录创建一个项目,配置Spring MVC,编写接口程序. 创建项目 打开IntelliJ IDEA,点击"File-> ...
 - jquery调用spring mvc接口返回字符串匹配
		
背景:有个增删改页面,用jquery祭出ajax异步调用接口,spring mvc响应对象是个json字符串,jquery根据响应结果判断,如果删除成功给出提示.那么问题来了,接口里响应的字符串怎么匹 ...
 - Spring mvc 接口枚举类型数据格式化处理
		
一.背景简述 首先,我们都知道枚举实例有两个默认属性,name 和 ordinal,可通过 name()和ordinal()方法分别获得.其中 name 为枚举字面量(如 MALE,FEMALE),o ...
 - JUnit测试,获取Spring MVC环境
		
@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextConfiguration(locations = { &qu ...
 - Spring MVC测试框架
		
原文链接:http://jinnianshilongnian.iteye.com/blog/2004660 Spring MVC测试框架详解——服务端测试 博客分类: springmvc杂谈 spri ...
 - Spring MVC测试框架详解——服务端测试
		
随着RESTful Web Service的流行,测试对外的Service是否满足期望也变的必要的.从Spring 3.2开始Spring了Spring Web测试框架,如果版本低于3.2,请使用sp ...
 - mock 测试 MVC
		
SpringMVC测试框架 基于RESTful风格的SpringMVC的测试,我们可以测试完整的Spring MVC流程,即从URL请求到控制器处理,再到视图渲染都可以测试. 一 MockMvcBui ...
 - 使用Mock测试
		
一.前言 在前面的章节我们介绍过 Junit 的使用,也了解过 spring-test,今天我们来了解一个新玩意 -- mock 测试.这里仅仅做一个入门,对返回视图和返回 Json 数据的方法进行测 ...
 
随机推荐
- 测试JsonAnalyzer2的12个测试用例:
			
测试用例如下: 01. Compact json text={"status":"","message":"success&quo ...
 - OneDrive 折腾记
			
起因 百度云的一系列劝退操作 OneDrive 5T 有点香 OneDrive 介绍 OneDrive有两种, 个人版 OneDrive 和 教育企业版 OneDrive for Business 个 ...
 - leetcode刷题-70爬楼梯
			
题目 假设你正在爬楼梯.需要 n 阶你才能到达楼顶. 每次你可以爬 1 或 2 个台阶.你有多少种不同的方法可以爬到楼顶呢? 注意:给定 n 是一个正整数. 思路 最开始使用的是回溯的方法,但是时间效 ...
 - 前端插入date类型的数据到数据库
			
//插入 @Override public boolean insertEmp(Emp emp) { String sql = "insert into emp(lwlEmpno,lwlEn ...
 - 在 Windows 上安装 Composer
			
a.去官网 getcomposer.org 下载安装程序 b.运行安装程序,需要开启三个扩展 openssl.curl.mbstring,没有开启的话 composer 也可以帮助开启:会自动将com ...
 - C#托管堆和非托管堆
 - 生成token和获取token
			
1.先安装模块pip install itsdangerous 举个例子:一个用户登录成功后,讲username和token作为key,value写到redis里面,判断是否失效(1.时间到了失效,2 ...
 - 原来写插件还可以选MEF
			
MEF是微软提供的一个轻量级的ICO容器,可以轻易的解除程序集的依赖关系,最近想写个类似插件试的软件所以搜索了一下,终于淘到宝了. 下面我们看看MEF是如何解耦的 新建一个控制台项目两个类库 Ites ...
 - top、ps -ef、ps aux的区别及内容详解
			
1.top和ps的区别 ps是静态查看进程--------top是动态(持续监控)进程 ps只是查看进程-----------top还可以监视系统性能,如平均负载,cpu和内存的消耗 2.ps -ef ...
 - 关于KeePass实现mstsc远程桌面(rdp协议)的自动登录
			
本文的Keepass版本:KeePass Password Safe Version 2.45 首先介绍一下Keepass,引用官网的解释如下: KeePass is a free open sour ...