Mock测试你的Spring MVC接口

1. 前言

在Java开发中接触的开发者大多数不太注重对接口的测试,结果在联调对接中出现各类问题。也有的使用Postman等工具进行测试,虽然在使用上没有什么问题,若是接口增长了权限测试起来就比较恶心了。因此建议在单元测试中测试接口,保证在交付前先自测接口的健壮性。今天就来分享一下胖哥在开发中是如何对Spring MVC接口进行测试的。java

在开始前请务必确认添加了Spring Boot Test相关的组件,在最新的版本中应该包含如下依赖:git

<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下进行的。github

2. 单独测试控制层

若是咱们只须要对控制层接口(Controller)进行测试,且该接口不依赖@Service@Component等注解声明的Spring Bean时,能够借助@WebMvcTest来启用只针对Web控制层的测试,例如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());
    }

}

这种方式要快的多,它只加载了应用程序的一小部分。可是若是你涉及到服务层这种方式是不凑效的,咱们就须要另外一种方式了。spring

3. 总体测试

大多数Spring Boot下的接口测试是总体而又全面的测试,涉及到控制层、服务层、持久层等方方面面,因此须要加载比较完整的Spring Boot上下文。这时咱们能够这样作,声明一个抽象的测试基类:json

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。api

而后针对具体的控制层进行以下测试代码的编写:服务器

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的调用,这样可使得测试速度快、不依赖网络环境,并且提供了一套验证的工具,这样可使得请求的验证统一并且很方便。接下来咱们来一步步构造一个测试的模拟请求,假设咱们存在一个下面这样的接口:mvc

@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是咱们必然会选择的,而andDoandReturn在某些场景下会有用,它们两个是可选的。咱们把上面的连在一块儿。

@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 获取更多资讯

我的博客:https://felord.cn

相关文章
相关标签/搜索