zoukankan      html  css  js  c++  java
  • 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是我们必然会选择的,而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

  • 相关阅读:
    序列化-请求数据校验。。。。。。
    python自动化测试-D5-学习笔记之二(常用模块之加密模块)
    python自动化测试-D5-学习笔记之二(常用模块之os,sys,random,string,time)
    python自动化测试-D5-学习笔记之二(常用模块之json模块)
    python自动化测试-D5-学习笔记之一(argv的使用)
    python自动化测试-D5-学习笔记之一(函数补充,内置函数,map,filter,eval)
    python习题:写一个函数打印两个字典中不一样的key和value
    python习题:用文件方式编写购物车程序,添加,查看和删除
    python习题:时间格式转换
    python习题:双色球
  • 原文地址:https://www.cnblogs.com/felordcn/p/13823833.html
Copyright © 2011-2022 走看看