网站首页 > java教程 正文
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往往需要这种工程化的单元测试来支撑。好了今天的分享就到这里,我是:码农小胖哥,多多关注,多多支持。
点击了解更多更加精彩。
猜你喜欢
- 2025-07-08 还在用策略模式解决 if-else?Map+函数式接口方法才是YYDS
- 2025-07-08 干掉OpenFeign,SpringBoot 3.0 自带的 HTTP 客户端真香!
- 2025-07-08 java开发三年,Java中接口的使用你得知道,不然你凭什么涨薪
- 2025-07-08 Java的类与接口(java接口和接口实现类)
- 2025-07-08 如何理解Java中接口存在的意义(java中接口有什么用)
- 2025-07-08 面试:如何保证接口的幂等性?常见的实现方案有哪些?
- 2025-07-08 java实现接口防刷(java接口防刷策略)
- 2025-07-08 java 9新特性 接口方法私有化(java私有属性怎么调用)
- 2025-07-08 妙用Java 8中的 Function接口(java里function的作用)
- 2025-07-08 调用AI接口API,需要遵循以下步骤
你 发表评论:
欢迎- 最近发表
-
- 搞趣网:我的世界全新皮肤包原始居民下载地址
- 我的世界拔刀剑MOD下载(我的世界拔刀剑mod下载国际版)
- 我的世界无正版账号的简单联机方法(非网易版,仅适用于局域网)
- 一些可以显著提高大型 Java 项目启动速度的尝试
- 常见的java敏感异常介绍(java 常见的异常)
- Java 开发者必看!三招实现外部 Jar 包动态加载(含热更新方案)
- Java JAR 启动内存参数配置指南:从基础设置到性能优化
- 对Spring MVC接口进行Mock测试(springmvc对外接口)
- 还在用策略模式解决 if-else?Map+函数式接口方法才是YYDS
- 干掉OpenFeign,SpringBoot 3.0 自带的 HTTP 客户端真香!
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)