☰
Spring Boot接口测试实战:MockMvc常用写法与坑总结
2026/9/26 11:55:14 网站建设 项目流程

MockMvc测试Spring Boot接口越写越顺手,这半年在项目里把GET和POST的各类场景基本都过了一遍,包括单个参数、多个参数、对象绑定、JSON请求体,踩了一些坑也总结了一些比较稳的写法。这篇就把实际项目中常用的MockMvc用法整理出来,从环境准备到各种参数场景的测试写法再到常见问题排查,完整走一遍,新手可以照着抄,老手也能看看有没有值得捡的细节。

1. 内容整体设计与思路拆解

1.1 为什么接口测试要选MockMvc

做后端接口开发的时候,最常遇到的尴尬是: Controller写完了,但前后端还没联调,总不能每次都启动整个Spring Boot应用再拿Postman敲一遍。启动慢、依赖多、环境还可能不干净,万一中间件没起来,接口测都没法测。

MockMvc解决的就是这个问题。它的核心思路是在不启动真实HTTP服务器的情况下,由Spring框架模拟MVC请求的完整流程——请求从MockMvc发出,经过DispatcherServlet分发、HandlerInterceptor拦截、参数解析、Controller处理,最后生成响应结果。整个过程和真实请求几乎一致,但又在内存里完成,速度非常快。

我在实际项目中用它做接口测试的体验是:构建请求的代码行数比Postman里点一堆配置还少,而且测试一旦通过,后续改动引入了回归问题马上就能暴露。举个具体例子,有一次改动了一个查询接口的分页逻辑,觉得改动很小,结果跑测试时发现旧的测试用例直接被干翻了,很快就定位到是参数校验的问题。这种反馈速度是手工测试很难给到的。

1.2 MockMvc适合哪些场景

从适用场景来看,MockMvc最拿手的是:

  • Controller层的功能测试,验证请求参数解析、校验、响应状态码、响应体结构是否正确
  • 鉴权、拦截器逻辑的验证,比如测试带token和不带token时请求是否被正确拦截
  • 异常处理机制的验证,包括自定义异常、全局异常处理器兜底的情况
  • 配合Spring Security做权限控制测试,你只需要构造一个带认证信息的请求即可

如果是纯SDK方法或者Service层逻辑,那不该用MockMvc,直接用单元测试跑方法就行。MockMvc定位在“站在Controller门口测试入口行为”,再往下的逻辑由更细粒度的单元测试去覆盖。这样分层的测试策略在实际项目里维护成本最低。

2. 环境准备与核心依赖配置

2.1 Spring Boot版本与依赖选择

MockMvc在Spring Boot项目里不需要额外引入独立的库,spring-boot-starter-test 里已经包含了。不过版本差异会导致写法和行为有些区别,尤其是Spring Boot 2.4之后,过时的MockMvc构建方式逐渐被替代。

我用的是Spring Boot 2.7.18,这个版本相对稳定,而且兼容性比较好,网上资料也最多。如果是Spring Boot 3.x,注意javax包名会变成jakarta,但测试框架层面的核心API基本一致,改包名之后大多能直接跑起来。

在pom.xml里加上:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>

spring-boot-starter-test是一个聚合依赖,它打包了JUnit 5、AssertJ、Hamcrest、Mockito、JSONassert、JsonPath等测试工具,MockMvc也在里面。不需要额外配版本号,跟着Spring Boot的BOM走就行。

2.2 在测试类中注入MockMvc

有两种用法:

第一种是类上注解加自动注入:

@SpringBootTest @AutoConfigureMockMvc class UserControllerTest { @Autowired private MockMvc mockMvc; }

@SpringBootTest会拉起完整的Spring应用上下文,@AutoConfigureMockMvc负责把MockMvc自动化配置好并注册到容器里。这种方式的测试覆盖范围最全,适合做接口级测试。

第二种是手动构建,不加载Spring上下文:

class UserControllerTest { @Test void testGet() { MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build(); // ... } }

手动方式速度快,但缺失了拦截器、过滤器、全局异常处理等机制,适合快速验证单个Controller逻辑,不适合做完整的接口链路测试。

实际项目的建议是:接口基础测试用@SpringBootTest + @AutoConfigureMockMvc,这样才能覆盖到过滤器和拦截器;如果只调试单个方法的参数绑定,才用standaloneSetup。我一般两种搭配使用,前者保底,后者查问题。

3. GET接口测试:从单参数到多参数

3.1 MockMvc发起GET请求的基本结构

先看一个最简单的GET接口:

@RestController @RequestMapping("/api/user") public class UserController { @GetMapping("/detail") public Result<UserVO> detail(@RequestParam Long id) { // 业务逻辑省略 return Result.success(userService.getById(id)); } }

对应的MockMvc测试:

@Test void testGetDetail() throws Exception { mockMvc.perform( MockMvcRequestBuilders.get("/api/user/detail") .param("id", "1001") .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200)) .andExpect(MockMvcResultMatchers.jsonPath("$.data.name").value("张三")); }

这里有几个关键点:

param("id", "1001")会把参数拼成QueryString,Spring MVC的@RequestParam就能拿到。注意param的value是String类型,框架会自动做类型转换,所以传数字时写字符串就可以。

.andExpect()里用的是一连串的静态方法,一般都会使用MockMvcRequestBuilders和MockMvcResultMatchers这两个类,很多人会采取静态导入简化代码,不过为了可读性,项目里可以约定保留前缀或全部静态导包方式,保持一致即可。

3.2 GET接口单个参数的各种写法

实际项目中GET接口参数不止一种,有些用@RequestParam,有些用@PathVariable,还有些直接绑定一个DTO对象。三种写法分别对应不同的MockMvc请求方式。

@PathVariable风格的接口:

@GetMapping("/detail/{id}") public Result<UserVO> detail(@PathVariable Long id) { return Result.success(userService.getById(id)); }

测试时要把ID拼在URL里:

mockMvc.perform( get("/api/user/detail/1001") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.id").value(1001));

这种方式在做RESTful风格的接口时特别常见。我习惯把测试覆盖三种情况:正常ID返回成功、非数字ID返回400或参数转换异常被全局异常处理器加工后的结果、不存在的ID返回404或对应业务错误码。这三个用例加起来基本能锁死接口的边界行为。

3.3 GET接口多个参数的测试方式

多参数场景更贴近真实业务,比如列表查询接口通常包含分页和筛选条件:

@GetMapping("/list") public Result<PageResult<UserVO>> list(@RequestParam Integer page, @RequestParam Integer size, @RequestParam(required = false) String keyword, @RequestParam(required = false) Integer status) { return Result.success(userService.pageQuery(page, size, keyword, status)); }

MockMvc测试多参数的时候,一个.param()叠加平铺下去就行:

@Test void testGetList() throws Exception { mockMvc.perform( get("/api/user/list") .param("page", "1") .param("size", "10") .param("keyword", "张") .param("status", "1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.total").isNumber()); }

这里有个细节容易忽略:param("page", "1")的第二个字符串会被框架用逗号分割成多个值吗?不会,除非value里真的包含英文逗号。所以当某个参数业务上需要传多个值给List<Long>接收的时候,就没法用单个param了,得用param("ids", "1,2,3")或者连写多个param参数,两种写法框架都能正确绑定。

多次实验下来,多参数的GET测试最需要注意的是required = false的字段。测试用例里应该专门设计一个“不传可选参数”的用例,确保接口在缺少可选参数时不会报500。比如上面这个接口,keyword和status都是可选的,那至少要跑一次只传分页参数的情况。

3.4 GET接口对象绑定的测试

Spring MVC支持把QueryString参数直接绑定到一个POJO对象上,比如:

@GetMapping("/search") public Result<List<UserVO>> search(UserQuery query) { return Result.success(userService.search(query)); }

这里的UserQuery是一个普通的POJO,包含page、size、keyword等字段。MockMvc测试时构建请求的方式和多个@RequestParam一样:

mockMvc.perform( get("/api/user/search") .param("page", "1") .param("size", "10") .param("keyword", "测试")) .andExpect(status().isOk());

Spring只是把参数解析后按照字段名映射到对象的属性里,所以param的key要跟POJO的字段名一一对应。这个绑定过程在MockMvc中没有任何特殊代码,反而是最省事的一种多参数形式。

4. POST接口测试:表单参数与JSON请求体

4.1 POST表单参数(application/x-www-form-urlencoded)

POST请求最常见的两种数据格式是表单提交和JSON。先看表单场景:

@PostMapping("/save") public Result<Long> save(@RequestParam String name, @RequestParam Integer age, @RequestParam String email) { return Result.success(userService.save(name, age, email)); }

MockMvc中用.param()直接追加参数,然后指定contentType为表单格式:

mockMvc.perform( post("/api/user/save") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("name", "李四") .param("age", "28") .param("email", "lisi@example.com")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200));

注意有个小坑:当contentType是表单格式的时候,MockMvc会自动把param放到请求体里并编码成name=李四&age=28的形式,不用手动去拼这个字符串。如果手动用.content("name=李四&age=28"),记得对中文做URL编码,不然容易乱码。我在早期就吃过这个亏,手动拼body传中文,结果接口收到的全是乱码。

4.2 POST JSON请求体的测试

现在的项目更流行前后端分离,接口大部分用JSON作为数据交换格式。场景变成这样:

@PostMapping("/add") public Result<Long> add(@RequestBody UserAddDTO dto) { return Result.success(userService.add(dto)); }

DTO结构:

public class UserAddDTO { private String name; private Integer age; private String email; // getter/setter 省略 }

MockMvc测试JSON请求体时,需要把对象序列化成JSON字符串,再放到.content()里:

@Test void testAddUser() throws Exception { UserAddDTO dto = new UserAddDTO(); dto.setName("王五"); dto.setAge(30); dto.setEmail("wangwu@example.com"); ObjectMapper objectMapper = new ObjectMapper(); String json = objectMapper.writeValueAsString(dto); mockMvc.perform( post("/api/user/add") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").isNumber()); }

用ObjectMapper把DTO序列化成JSON,是项目里最稳的方案。如果是固定测试数据,也可以直接写JSON字符串,但对象序列化的好处是改字段时IDE会提醒同步改,减少遗漏。

4.3 JSON请求体含多个嵌套对象的场景

真实业务中POST接口经常要接收复杂对象,比如包含子对象或列表:

@PostMapping("/batch") public Result<Boolean> batchCreate(@RequestBody UserBatchCreateDTO batchDTO) { return Result.success(userService.batchCreate(batchDTO)); }

UserBatchCreateDTO除了基本字段还有列表:

public class UserBatchCreateDTO { private Long groupId; private List<UserAddDTO> users; // getter/setter 省略 }

这种场景用对象序列化特别舒服:

@Test void testBatchCreate() throws Exception { UserBatchCreateDTO batchDTO = new UserBatchCreateDTO(); batchDTO.setGroupId(101L); UserAddDTO user1 = new UserAddDTO(); user1.setName("赵六"); user1.setAge(25); user1.setEmail("zhaoliu@example.com"); UserAddDTO user2 = new UserAddDTO(); user2.setName("孙七"); user2.setAge(26); user2.setEmail("sunqi@example.com"); batchDTO.setUsers(Arrays.asList(user1, user2)); String json = new ObjectMapper().writeValueAsString(batchDTO); mockMvc.perform( post("/api/user/batch") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); }

多次实践后我的心得是,只要接口里出现@RequestBody,一律用对象序列化。纯粹手写JSON字符串在字段多、嵌套深的时候容易漏逗号、引号错位,而且一旦接口字段调整,手写的JSON可能完全没有感知。

4.4 模拟Ajax请求参数赋值的实战坑

开发中经常遇到前端用jQuery或者axios的POST请求,实际请求头里可能会带X-Requested-With: XMLHttpRequest,用来标识这是一个Ajax请求。有些后端代码会判断这个头做不同的逻辑,比如返回JSON而不是跳转页面,或者做日志记录。

MockMvc里模拟Ajax请求很简单:

mockMvc.perform( post("/api/user/add") .header("X-Requested-With", "XMLHttpRequest") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk());

如果你在开发接口时遇到坑,比如请求明明能通,但前端Aajx请求总是走到奇怪的逻辑分支,那多半是后端代码里读取了请求头但测试时没模拟。我建议测试用例中把前端真实会带的header都带上,保持测试环境和真实调用的一致性。之前在某项目中,后端用Shiro做管理后台的登录判断,根据X-Requested-With来决定返回JSON还是跳转HTML,测试用例里不加这个header,测出来的结果和真实前端表现完全不一样。

4.5 发送PUT、DELETE等其它方法

MockMvcRequestBuilders框架支持所有HTTP方法,写法上只是换掉方法名:

mockMvc.perform(put("/api/user/update").contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isOk()); mockMvc.perform(delete("/api/user/1001")) .andExpect(status().isOk());

不过很多团队对PUT/DELETE的测试写法其实和POST一样,只是换了请求方法。如果你不想频繁切换,也可以统一用request()方法指定HTTP method,灵活性更高,但代码可读性稍差。我个人的习惯是项目里统一用get/post/put/delete这种直观写法,让别人看测试用例的时候一眼就知道被测接口的方法类型。

5. 响应断言与实际结果查看

5.1 状态码与响应体断言

MockMvc测试里断言是最重要的环节,不写断言的测试等于没测。最基础的是状态码断言:

.andExpect(status().isOk()) .andExpect(status().isBadRequest()) .andExpect(status().isInternalServerError())

然后是用jsonPath断言响应体字段。Spring Boot内置了Jayway JsonPath库,表达式写起来和JSONPath语法一致:

.andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.message").value("成功")) .andExpect(jsonPath("$.data").isNotEmpty())

多个断言可以一直往后面追加。jsonPath还支持判断数组长度:

.andExpect(jsonPath("$.data.records").isArray()) .andExpect(jsonPath("$.data.records.length()").value(2)) .andExpect(jsonPath("$.data.records[0].name").value("赵六"))

5.2 控制台查看完整请求参数与响应

有段时间我在调试一个复杂的POST接口,请求参数没问题但后端就是解析不到值,为了排查,我直接在MockMvc测试里把请求和响应打印出来。办法很简单:

mockMvc.perform( post("/api/user/add") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andDo(MockMvcResultHandlers.print()) .andExpect(status().isOk());

print()会把整个请求过程输出到控制台,包括请求头、请求体、响应头、响应体、视图解析结果等。我调试时常用这招快速看请求参数是否真的传到了框架里,尤其是中文编码对不对、JSON有没有被正确解析,一眼就能看到。

如果想要更精细的日志,可以用ResultHandler自定义。但多数场景下print()就够了,我自己很少需要再写自定义的打印逻辑。

5.3 提取响应结果进行后续断言

有时候一个测试里需要先调用A接口拿到返回的ID,再调用B接口做依赖操作。MockMvc提供了一个MvcResult对象来承接响应结果:

MvcResult result = mockMvc.perform( post("/api/user/add") .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) .andReturn(); String responseBody = result.getResponse().getContentAsString(); // 用JsonPath解析ID long userId = JsonPath.parse(responseBody).read("$.data", Long.class); // 接着调GET接口 mockMvc.perform(get("/api/user/detail").param("id", String.valueOf(userId))) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.id").value(userId));

andReturn()是连接多个请求的枢纽。在集成测试中这种方法特别实用,能在一个测试方法里走完“创建-查询-更新-删除”的完整链表,比拆成多个孤立测试更贴近真实调用链路。

5.4 文件上传接口的测试

文件上传在MockMvc中也有内建支持,场景是MultipartFile参数:

@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { return Result.success(fileService.store(file)); }

测试写法:

MockMultipartFile file = new MockMultipartFile( "file", // 参数名 "test.txt", // 原始文件名 MediaType.TEXT_PLAIN_VALUE, "测试文件内容".getBytes(StandardCharsets.UTF_8) ); mockMvc.perform( multipart("/api/upload/") .file(file) .header("X-Requested-With", "XMLHttpRequest")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200));

MockMultipartFile是MockMvc提供的辅助类,字节内容在测试中直接写在内存里,不需要真的去读磁盘文件,测试跑得很快。如果需要模拟一个超大文件,只需把字节数组撑大即可,方便测接口的容量限制逻辑。

6. 常见问题与排查技巧实录

6.1 MockMvc测试中文乱码怎么办

这是最常踩的坑。表现是接口返回的JSON中文变成???或\u5f20\u4e09。

原因通常是服务端响应没有正确设置编码,和MockMvc本身关系不大,只是测试环境更容易暴露出来。推荐在全局配置里加一个消息转换器配置:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); // 或者用 Jackson 的 MappingJackson2HttpMessageConverter converter.setDefaultCharset(StandardCharsets.UTF_8); converters.add(converter); } }

如果用的是Spring Boot默认Jackson,更简单的方案是在application.yml里设置:

spring: http: encoding: charset: UTF-8 enabled: true force: true

注意force: true很关键,它强制请求和响应都使用UTF-8编码。测试时如果接口固定返回JSON,也可以直接contentType加charset=UTF-8。

6.2 每次跑测试都启动整个Spring上下文,太慢怎么办

@SpringBootTest会启动完整的应用上下文,一旦项目依赖了数据库、Redis、MQ,加载时间会明显变长。有几个优化思路:

第一步是尽量保证测试配置里排除不需要的自动配置,比如用@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK),这是默认值,只启动WebApplicationContext不启动内嵌服务器。

第二步是给测试都加上@ActiveProfiles("test"),使用独立的测试配置和内存数据库,避免连到开发环境的中间件。

第三步才是终极解——如果只是想测Controller的参数解析,不关心数据库或缓存,可以专门写轻量测试类,用@WebMvcTest只加载Web层需要的组件。@WebMvcTest会扫描Controller、ControllerAdvice、Filter等,但不会加载Service、Mapper,需要自己用@MockBean去Mock掉依赖。

我自己的项目一般分层设计:Web层用@WebMvcTest保证速度和纯净度,接口集成场景用@SpringBootTest做全链路验证。两种配合基本能兼顾速度和覆盖率。

6.3 jsonPath断言一直报错,提示找不到节点

jsonPath断言失败大多是响应体结构和预期不符。常见原因有三类。

一类是响应体实际是JSON字符串而不是JSON对象,比如接口直接把String类型的JSON结果返回了,此时jsonPath解析的根节点就是一个字符串,$.code自然找不到。处理办法是先看一下print()输出的原始响应,确认结构。

另一类是存在统一响应包装,但包装字段是data还是result,写错了路径就找不到。我经常建议测试代码里把断言路径和实际响应体对照一遍,尤其当响应包装类字段改名后,很容易漏改测试。

还有一类是列表场景,数组的长度断言在JsonPath里是$.data.length(),但有些人容易写成$.data.length、$.data.size()这种,都会失败。不确定写法时先跑一次print,实际路径一目了然。

6.4 接口返回401或302,测试通过但真实调用正常

这种情况大概率是测试请求没有带上认证信息。如果项目里用了Spring Security,需要模拟认证用户。常见操作是在测试类上添加:

@WithMockUser(username = "admin", roles = "ADMIN")

这个注解只对当前测试方法或测试类生效,MockMvc会注入对应Authentication。如果项目用的是自定义Token方案,通过请求头模拟更简单:

.header("Authorization", "Bearer " + token)

测试的准备阶段可以先从MockMvc或测试代码里生成token,也可以在测试前置里直接调登录接口。我个人的建议是:凡是涉及认证的接口,每个测试注释里最好标一句“需要认证角色”,这样后续接手的人不会莫名其妙跳过认证相关的分支。

6.5 多个接口共用MockMvc,代码冗余严重

测试代码写多了之后,会发现大量重复的mockMvc.perform()样板代码。我的做法是抽一个基类或者工具类:

public abstract class BaseMockMvcTest { @Autowired protected MockMvc mockMvc; @Autowired protected ObjectMapper objectMapper; protected ResultActions getRequest(String url, Map<String, Object> params) throws Exception { MockHttpServletRequestBuilder builder = get(url); params.forEach((k, v) -> builder.param(k, String.valueOf(v))); return mockMvc.perform(builder); } protected ResultActions postJson(String url, Object body) throws Exception { return mockMvc.perform( post(url) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(body))); } }

这样每个测试类继承基类之后,一个复杂请求只需要一两行就能发起。长期维护下来,接口改动时只需调整业务断言部分,请求构建的逻辑统一在一个地方改,效率会提升很多。

6.6 测试数据污染问题

MockMvc测试和单元测试不同,它走的是真实接口链路,很可能干到数据库里的数据。同一条测试跑第二次可能就报唯一约束冲突,或者列表查询的结果数量不对。

解决方案我自己常用的有以下几种:

  • 测试前置方法里构造数据,测试后置方法里清空数据,尤其是针对主键、唯一索引字段的数据
  • 使用事务回滚思路,比如在测试方法上配合@Transactional,Spring会帮你回滚测试内产生的事务,但要注意有些框架(如部分ORM)默认事务行为不同,不一定完全覆盖
  • 对查询类接口,使用无关随机数构造数据,避免和其他用例冲突
  • 确认数据库连接用的是本地内存数据库或独立测试库,不要用开发环境的库

这些工作虽然有些繁琐,但接口测试的稳定性是后面持续集成的基石,数据污染不解决,测试跑着跑着就变成“红一片”然后被迫跳过,整体价值会严重缩水。

7. MockMvc测试的进一步扩展

7.1 与Mockito配合做依赖隔离

接口测试经常牵扯Service层,而Service层动不动就访问第三方平台、Redis、RPC。如果不想在Controller测试里把整个依赖链拉起来,可以用Mockito的@MockBean把依赖Mock掉:

@WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; @Test void testGetDetail() throws Exception { UserVO vo = new UserVO(); vo.setId(1001L); vo.setName("测试用户"); when(userService.getById(1001L)).thenReturn(vo); mockMvc.perform(get("/api/user/detail").param("id", "1001")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.name").value("测试用户")); } }

这样测试速度会快很多,而且能隔离底层失败带来的干扰。但也要明确一点:如果Service逻辑还没测过,@MockBean会掩盖真实Service的问题。所以我的策略是Controller测试和Service单元测试分开写,各司其职。

7.2 参数校验失败场景的测试

Spring Validation在Controller参数上非常常见,比如:

@PostMapping("/add") public Result<Long> add(@Validated @RequestBody UserAddDTO dto) { return Result.success(userService.add(dto)); }

DTO字段上的校验注解:

public class UserAddDTO { @NotBlank(message = "姓名不能为空") private String name; @Min(value = 1, message = "年龄不合法") @Max(value = 120, message = "年龄不合法") private Integer age; @Email(message = "邮箱格式不正确") private String email; }

MockMvc测试校验逻辑只需要构造非法参数,然后断言返回400加错误消息:

@Test void testAddWithInvalidParam() throws Exception { UserAddDTO dto = new UserAddDTO(); dto.setName(""); dto.setAge(200); dto.setEmail("invalid-email"); mockMvc.perform( post("/api/user/add") .contentType(MediaType.APPLICATION_JSON) .content(new ObjectMapper().writeValueAsString(dto))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("邮箱格式"))); }

因为不同的全局异常处理器返回的响应结构不一样,断言时建议用containsString或者hasItem匹配,而不是硬编码整个错误消息,否则字段描述措辞一变测试就崩。

7.3 在MockMvc里测试异步接口

Spring Boot支持下异步Controller,返回值用Callable或者DeferredResult。MockMvc测试异步接口时会发现主线程返回了还没执行完,需要设置异步超时:

MvcResult result = mockMvc.perform( get("/api/async/task") .param("id", "1")) .andExpect(request().asyncStarted()) .andReturn(); mockMvc.perform(asyncDispatch(result)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data").value("done"));

这个用法相对少一些,但纯JAVA的异步接口回归测试一旦漏掉,出了问题很难排查。我建议有异步逻辑的项目至少保留一组这样的用例,避免后续改动异步模式时无测试守卫。

8. MockMvc测试维护的几个心得

代码写法学会了,最后分享一些测试长期稳定跑下来的经验。

第一,测试类不要一个文件装一堆接口。按Controller维度各建一个测试类,命名清晰如UserControllerTest、OrderControllerTest,不然后期改动时定位测试非常痛苦。

第二,每个接口至少覆盖三条路径:正常路径、参数缺失或非法路径、业务异常路径。很多时候大家写测试只写了第一条,过了一两周接口改签被破坏,测试却还绿的,等线上出了问题才反应过来。

第三,公共头部信息尽量封装。比如项目里很多接口需要带X-Requested-With、Authorization头,封装到工具方法里可以减少漏加的情况。我踩过好几次漏加请求头导致测试和线上表现不一致的坑。

第四,多参数场景的GET接口,建议把可选参数缺失的情况做成独立用例。这类接口改动频率高,漏测可能性也大,一旦漏测就容易出现“前端少传一个参数后端直接500”的线上事故。

第五,print()是排查利器但不是每次都要加。正式提交的测试用例尽量去掉print,保留在调试期间即可,不然CI日志会非常吵,而且大响应体打印还会增加不少IO开销。

最后说一下数据构造的思路。复杂嵌套对象不要在每个测试里new一遍,我习惯用构建器或者工厂方法生成通用测试对象,需要某个字段特殊值时再单独set。这样接口字段增多时,新增用例的成本更低,也更愿意去写测试。

这些内容是我在Spring Boot项目里用MockMvc做接口测试逐步积累下来的,不敢说覆盖到所有极端情况,但日常开发中单人开发和团队协作里该碰到的核心场景都在里面了。如果项目里的接口文档和参数结构已经相对稳定,照着这套写法把测试补上,后续接口改动带回归的几率会小很多。调试过程中如果遇到特殊的坑,建议从print()响应开始排查,再逐步对照Spring MVC的参数解析流程,大多数问题都能找到根因。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询