这可能是解决你Spring MVC接口漏洞百出的关键

开发 后端
在 Java 开发中接触的开发者大多数不太注重对接口的测试,结果在联调对接中出现各种问题。也有的使用 Postman 等工具进行测试,虽然在使用上没有什么问题,如果接口增加了权限测试起来就比较恶心了。

[[346147]]

1. 前言

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

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

  1. <dependency> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.     <artifactId>spring-boot-starter-test</artifactId> 
  4.     <scope>test</scope> 
  5.     <exclusions> 
  6.         <exclusion> 
  7.             <groupId>org.junit.vintage</groupId> 
  8.             <artifactId>junit-vintage-engine</artifactId> 
  9.         </exclusion> 
  10.     </exclusions> 
  11. </dependency> 

本文是在Spring Boot 2.3.4.RELEASE下进行的。

2. 单独测试控制层

如果我们只需要对控制层接口(Controller)进行测试,且该接口不依赖@Service、@Component等注解声明的 Spring Bean 时,可以借助@WebMvcTest来启用只针对 Web 控制层的测试,例如

  1. @WebMvcTest 
  2. class CustomSpringInjectApplicationTests { 
  3.     @Autowired 
  4.     MockMvc mockMvc; 
  5.  
  6.     @SneakyThrows 
  7.     @Test 
  8.     void contextLoads() { 
  9.         mockMvc.perform(MockMvcRequestBuilders.get("/foo/map")) 
  10.                 .andExpect(ResultMatcher.matchAll(status().isOk(), 
  11.                         content().contentType(MediaType.APPLICATION_JSON), 
  12.                         jsonPath("$.test"Is.is("hello")))) 
  13.                 .andDo(MockMvcResultHandlers.print()); 
  14.     } 
  15.  

这种方式要快的多,它只加载了应用程序的一小部分。但是如果你涉及到服务层这种方式是不凑效的,我们就需要整体测试了方了。

3. 整体测试

大多数 Spring Boot 下的接口测试是整体而又全面的测试,涉及到控制层、服务层、持久层等方方面面,所以需要加载比较完整的 Spring Boot 上下文。这时我们可以这样做,声明一个抽象的测试基类:

  1. package cn.felord.custom; 
  2.  
  3. import org.springframework.beans.factory.annotation.Autowired; 
  4. import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 
  5. import org.springframework.boot.test.context.SpringBootTest; 
  6. import org.springframework.test.web.servlet.MockMvc; 
  7.  
  8.  
  9. /** 
  10.  * 测试基类, 
  11.  * @author felord.cn 
  12.  */ 
  13. @SpringBootTest 
  14. @AutoConfigureMockMvc 
  15. abstract class CustomSpringInjectApplicationTests { 
  16.     /** 
  17.      * The Mock mvc. 
  18.      */ 
  19.     @Autowired 
  20.     MockMvc mockMvc; 
  21.     // 其它公共依赖和处理方法 

只有当@AutoConfigureMockMvc存在时MockMvc才会被注入 Spring IoC。

然后针对具体的控制层进行如下测试代码的编写:

  1. package cn.felord.custom; 
  2.  
  3. import lombok.SneakyThrows; 
  4. import org.hamcrest.core.Is
  5. import org.junit.jupiter.api.Test; 
  6. import org.springframework.http.MediaType; 
  7. import org.springframework.test.web.servlet.ResultMatcher; 
  8. import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 
  9. import org.springframework.test.web.servlet.result.MockMvcResultHandlers; 
  10.  
  11. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 
  12.  
  13. /** 
  14.  * 测试FooController. 
  15.  * 
  16.  * @author felord.cn 
  17.  */ 
  18. public class FooTests extends CustomSpringInjectApplicationTests { 
  19.     /** 
  20.      * /foo/map接口测试. 
  21.      */ 
  22.     @SneakyThrows 
  23.     @Test 
  24.     void contextLoads() { 
  25.         mockMvc.perform(MockMvcRequestBuilders.get("/foo/map")) 
  26.                 .andExpect(ResultMatcher.matchAll(status().isOk(), 
  27.                         content().contentType(MediaType.APPLICATION_JSON), 
  28.                         jsonPath("$.test"Is.is("bar")))) 
  29.                 .andDo(MockMvcResultHandlers.print()); 
  30.     } 

4. MockMvc 测试

集成测试时,希望能够通过输入 URL 对 Controller 进行测试,如果通过启动服务器,建立 http client 进行测试,这样会使得测试变得很麻烦,比如,启动速度慢,测试验证不方便,依赖网络环境等,为了可以对 Controller 进行测试,所以引入了MockMvc。

MockMvc实现了对 Http 请求的模拟,能够直接使用网络的形式,转换到 Controller 的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。接下来我们来一步步构造一个测试的模拟请求,假设我们存在一个下面这样的接口:

  1. @RestController 
  2. @RequestMapping("/foo"
  3. public class FooController { 
  4.     @Autowired 
  5.     private MyBean myBean; 
  6.  
  7.     @GetMapping("/user"
  8.     public Map<String, String> bar(@RequestHeader("Api-Version") String apiVersion, User user) { 
  9.         Map<String, String> map = new HashMap<>(); 
  10.         map.put("test", myBean.bar()); 
  11.         map.put("version", apiVersion); 
  12.         map.put("username"user.getName()); 
  13.         //todo your business 
  14.         return map; 
  15.     } 

参数设定为name=felord.cn&age=18,那么对应的 HTTP 报文是这样的:

  1. GET /foo/user?name=felord.cn&age=18 HTTP/1.1 
  2. Host: localhost:8888 
  3. Api-Version: v1 

可以预见的返回值为:

  1.     "test""bar"
  2.     "version""v1"
  3.     "username""felord.cn" 

事实上对接口的测试可以分为以下几步。

构建请求

构建请求由MockMvcRequestBuilders负责,他提供了请求方法(Method),请求头(Header),请求体(Body),参数(Parameters),会话(Session)等所有请求的属性构建。/foo/user接口的请求可以转换为:

  1. MockMvcRequestBuilders.get("/foo/user"
  2.                 .param("name""felord.cn"
  3.                 .param("age""18"
  4.                 .header("Api-Version""v1"

执行 Mock 请求

然后由MockMvc执行 Mock 请求:

  1. mockMvc.perform(MockMvcRequestBuilders.get("/foo/user"
  2.                 .param("name""felord.cn"
  3.                 .param("age""18"
  4.                 .header("Api-Version""v1")) 

对结果进行处理

请求结果被封装到ResultActions对象中,它封装了多种让我们对 Mock 请求结果进行处理的方法。

对结果进行预期期望

ResultActions#andExpect(ResultMatcher matcher)方法负责对响应的结果的进行预期期望,看看是否符合测试的期望值。参数ResultMatcher负责从响应对象中提取我们需要期望的部位进行预期比对。

假如我们期望接口/foo/user返回的是JSON,并且 HTTP 状态为200,同时响应体包含了version=v1的值,我们应该这么声明:

  1. ResultMatcher.matchAll(MockMvcResultMatchers.status().isOk(), 
  2.                MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON), 
  3.                MockMvcResultMatchers.jsonPath("$.version"Is.is("v1"))); 

JsonPath是一个强大的 JSON 解析类库,请通过其项目仓库https://github.com/json-path/JsonPath了解。

对响应进行处理

ResultActions#andDo(ResultHandler handler)方法负责对整个请求/响应进行打印或者 log 输出、流输出,由MockMvcResultHandlers工具类提供这些方法。我们可以通过以上三种途径来查看请求响应的细节。

例如/foo/user接口:

  1. MockHttpServletRequest: 
  2.       HTTP Method = GET 
  3.       Request URI = /foo/user 
  4.        Parameters = {name=[felord.cn], age=[18]} 
  5.           Headers = [Api-Version:"v1"
  6.              Body = null 
  7.     Session Attrs = {} 
  8.  
  9. Handler: 
  10.              Type = cn.felord.xbean.config.FooController 
  11.            Method = cn.felord.xbean.config.FooController#urlEncode(String, Params) 
  12.  
  13. Async: 
  14.     Async started = false 
  15.      Async result = null 
  16.  
  17. Resolved Exception: 
  18.              Type = null 
  19.  
  20. ModelAndView: 
  21.         View name = null 
  22.              View = null 
  23.             Model = null 
  24.  
  25. FlashMap: 
  26.        Attributes = null 
  27.  
  28. MockHttpServletResponse: 
  29.            Status = 200 
  30.     Error message = null 
  31.           Headers = [Content-Type:"application/json"
  32.      Content type = application/json 
  33.              Body = {"test":"bar","version":"v1","username":"felord.cn"
  34.     Forwarded URL = null 
  35.    Redirected URL = null 
  36.           Cookies = [] 

获取返回结果

如果你希望进一步处理响应的结果,也可以通过ResultActions#andReturn()拿到MvcResult类型的结果进行进一步的处理。

完整的测试过程

通常andExpect是我们必然会选择的,而andDo和andReturn在某些场景下会有用,它们两个是可选的。我们把上面的连在一起。

  1. @Autowired 
  2. MockMvc mockMvc; 
  3.  
  4. @SneakyThrows 
  5. @Test 
  6. void contextLoads() { 
  7.  
  8.      mockMvc.perform(MockMvcRequestBuilders.get("/foo/user"
  9.             .param("name""felord.cn"
  10.             .param("age""18"
  11.             .header("Api-Version""v1")) 
  12.             .andExpect(ResultMatcher.matchAll(status().isOk(), 
  13.                     content().contentType(MediaType.APPLICATION_JSON), 
  14.                     jsonPath("$.version"Is.is("v1")))) 
  15.             .andDo(MockMvcResultHandlers.print()); 
  16.  

这种流式的接口单元测试从语义上看也是比较好理解的,你可以使用各种断言、正例、反例测试你的接口,最终让你的接口更加健壮。

5. 总结

一旦你熟练了这种方式,你编写的接口将更加具有权威性而不会再漏洞百出,甚至有时候你也可以使用 Mock 来设计接口,使之更加贴合业务。所以 CRUD 不是完全没有技术含量,高质量高效率的 CRUD 往往需要这种工程化的单元测试来支撑。

本文转载自微信公众号「码农小胖哥」,可以通过以下二维码关注。转载本文请联系码农小胖哥公众号。

 

责任编辑:武晓燕 来源: 码农小胖哥
相关推荐

2017-04-11 21:37:26

2009-12-15 09:04:40

Windows 7系统漏洞

2013-07-08 10:36:57

2016-10-26 10:20:22

2015-03-19 10:01:26

2017-02-20 15:51:07

2017-06-08 11:11:32

互联网

2018-07-16 10:10:43

WiFi上网网速

2021-08-27 10:14:22

机器学习工具手册人工智能

2021-11-03 16:10:16

RedisJava内存

2024-08-28 11:56:33

2018-10-25 09:37:02

Docker入门容器

2023-02-26 00:00:01

Spring数据库组件

2020-12-28 10:22:43

手机App青少年模式网络

2023-01-11 08:24:32

2023-02-08 10:39:09

ChatGPT论文人工智能

2023-02-26 10:14:51

Spring第三方库

2018-11-12 00:16:21

云计算行业科技

2020-04-14 10:50:47

FlutterGithub

2018-11-05 08:10:30

Netty架构模型
点赞
收藏

51CTO技术栈公众号