ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot接口测试实战:MockMvc从GET到POST的完整用法

Spring Boot接口测试实战:MockMvc从GET到POST的完整用法 1. 为什么是MockMvc而不是直接拿Postman点两下做Spring Boot开发的人十有八九都习惯打开Postman或者Apifox填好URL和参数点一下“Send”看到200就完事。但真正到了要验证控制器逻辑、参数绑定、JSON序列化、拦截器行为的时候Postman就显得不够用了——它不是不能测而是没法融入到自动化和回归测试体系里。这也是我要写这篇内容的核心原因MockMvc能在不启动真实Servlet容器的情况下完整模拟HTTP请求打到Spring MVC的DispatcherServlet上把控制器、参数解析、校验、拦截器、异常处理都跑一遍最后给你一个可以用来断言的MockHttpServletResponse。这里最关键的一点是MockMvc并不是真实起一个Tomcat、Jetty去监听端口而是直接在应用上下文内部走一遍MVC的核心流程。它和集成测试比如真正起Spring Boot的服务再发HTTP请求是有本质区别的——MockMvc更快、更轻、更适合单元级别和控制器级别的测试而真正的端到端测试得靠TestRestTemplate或者WebTestClient去访问真实端口。两者各有用武之地但针对“测接口逻辑”这个事情MockMvc是效率最高、定位最准的那把刀。适合谁来学这篇内容呢刚接触Spring Boot测试脚手架的人、在写毕业设计想补测试覆盖率的人、或者工作中需要给接口加个快速自测场景的开发者都能从中拿到可直接抄走用的代码片段。这篇内容我把GET和POST两种最常见的请求方式拆开讲单个参数、多个参数、路径参数、JSON body、表单提交、文件上传都会覆盖到配合一些我踩过的坑和排查经验尽量让内容落地而不是浮在理论表面。2. 环境准备与测试骨架搭建2.1 引入依赖注意starter-test的隐藏能力Spring Boot测试的第一步是在pom.xml里加上spring-boot-starter-test。这个依赖包的东西比很多人以为的要多——它不只包含JUnit和Mockito还打包了AssertJ、Hamcrest、JSONPath、JsonUnit这些工具。也就是说你写的断言可以既有JUnit的assertEquals习惯也可以用AssertJ的链式风格还可以在andExpect里用jsonPath直接写JSONPath表达式去校验响应body。这就是为什么很多时候MockMvc测试代码里不需要额外引入JSON解析库的原因。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency要注意的一个点是版本对应。如果你用的是Spring Boot 2.x系列JUnit默认是5.x也就是用org.junit.jupiter.api.Test这个包名的注解如果是很老的1.x项目走的是JUnit 4。大部分教程默认讲的都是Jupiter所以你在写测试类的时候导入的Test必须是org.junit.jupiter.api.Test这个包名和JUnit 4的那个org.junit.Test长得几乎一样特别容易导错一旦导错整个测试类的注解全部不生效直接报空指针或者根本跑不起来。2.2 WebMvcTest和SpringBootTest怎么选搭建测试骨架之前需要先想清楚切入方式。Spring Boot提供了两种主流姿势WebMvcTest和SpringBootTest配合AutoConfigureMockMvc。WebMvcTest(YourController.class)是切片测试它只会加载Web层的相关组件——Controller、ControllerAdvice、WebMvcConfigurer、Filter等Service和Repository不会装进上下文里。这套机制的好处是启动飞快测试范围聚焦坏处是Service层需要你自己用MockBean或者MockitoBean去模拟。如果你的控制器依赖的Service逻辑非常复杂爬数据、缓存、消息队列全在里面那么WebMvcTest配合Mock模拟是性价比最高的组合它只关心“Controller怎么接收参数、怎么调用Service、怎么把返回结果组装成JSON”不会因为数据库连接失败导致测试崩掉。SpringBootTest则是全量加载应用上下文所有Bean都真实装配。适合那种需要CompleteIntegration风格测试的场景比如你在测事务、测Repository和Controller的联动。代价就是启动慢而且一旦有外部依赖数据库、MQ、Redis不可用测试直接红。所以我的建议很明确只测Controller层逻辑用WebMvcTest要测整条链路再用SpringBootTest。本篇的代码示例以WebMvcTest为主因为这样能突出MockMvc本身的用法不被环境因素干扰。2.3 一个能直接跑的最小测试骨架先给一个最朴素的骨架后面所有场景的测试都可以在这个基础上扩展import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.test.web.servlet.MockMvc; import org.springframework.boot.test.mock.mockito.MockBean; import com.example.demo.controller.UserController; import com.example.demo.service.UserService; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; WebMvcTest(UserController.class) class UserControllerTest { Autowired private MockMvc mockMvc; MockBean private UserService userService; Test void testGetUserById() throws Exception { mockMvc.perform(get(/api/user/list)) .andExpect(status().isOk()); } }这里有个很容易踩的细节MockMvc本身不是让你自己new出来的对象而是由Spring Boot的自动配置在测试上下文里准备好的。如果你是老手以前在Spring MVC时代手动MockMvcBuilders.standaloneSetup(controller).build()那是另一个路数——它不需要Spring上下文只把目标控制器单独拉起来测速度极快但拦截器、过滤器、全局异常处理这些就都需要自己手动模拟。而WebMvcTest下注入的MockMvc是装配了整个Web环境的ControllerAdvice、过滤器链全都会生效这也是为什么我推荐用自动注入而不是手动构建。还有一个骨架阶段就要提醒自己的事WebMvcTest默认会扫描ControllerAdvice、RestControllerAdvice这些全局异常类并加载进去所以如果你项目里有全局异常处理器测试中触发异常路径时返回的JSON结构会被它影响。这是件好事但不懂的人会在排查时懵——明明控制器抛了异常响应码却是200因为异常处理器把所有异常都兜底转成了200。所以写测试断言时先看看项目里有没有全局异常处理再决定期望的HTTP状态码。3. GET接口测试单个参数、多个参数与路径参数3.1 单个请求参数param()的底层逻辑先从一个最常见但也最容易出错的写法开始。假设有一个接口定义是GetMapping(/api/user/detail)参数叫id类型是LongRestController RequestMapping(/api/user) public class UserController { GetMapping(/detail) public ResultUserVO detail(RequestParam(id) Long id) { // 业务逻辑 return Result.success(userService.getById(id)); } }MockMvc测试这个接口最简单的写法是用param()mockMvc.perform(get(/api/user/detail) .param(id, 1001)) .andExpect(status().isOk()) .andExpect(jsonPath($.data.name).value(张三));param()方法接收的是String类型的键值对。你也许会觉得“这不就是把URL拼一下吗”我可以直接写成get(/api/user/detail?id1001)。但两种写法有一个重要差异param(id, 1001)会对参数做URL编码处理而直接拼URL不会。比如参数值里带中文、空格、特殊符号直接拼URL很容易在后续andExpect断言时拿到一串乱码或者被过滤掉。param()则会把参数交给Servlet API的参数解析机制按application/x-www-form-urlencoded的规范处理中文、%号、号都能正确还原。所以我的建议是写MockMvc测试时尽量使用.param()不要自己手拼QueryString。3.2 多个GET参数链式param()与Map批量传参多个参数场景也常见比如列表接口带了关键词、页码、页大小GetMapping(/list) public ResultPageResultUserVO list(RequestParam(keyword) String keyword, RequestParam(page) Integer page, RequestParam(size) Integer size) { return Result.success(userService.page(keyword, page, size)); }MockMvc写多参数有几种写法。第一种就是链式调用直来直去mockMvc.perform(get(/api/user/list) .param(keyword, 张) .param(page, 1) .param(size, 10)) .andExpect(status().isOk()) .andExpect(jsonPath($.data.total).isNotEmpty());第二种姿势是把参数塞进MultiValueMap再丢进params()方法。这个适合参数特别多、或者你在测试里要写一组条件动态拼接的场景LinkedMultiValueMapString, String params new LinkedMultiValueMap(); params.add(keyword, 张); params.add(page, 1); params.add(size, 10); params.add(sort, create_time); params.add(order, desc); mockMvc.perform(get(/api/user/list) .params(params)) .andExpect(status().isOk());MultiValueMap支持同一个key绑定多个值这正好对应HTTP里面一个参数名带多个值的场景比如?ids1ids2ids3。如果你的接口参数定义是RequestParam(ids) ListLong ids用这个Map来传值是最稳的链式param()虽然也能追加同名参数但维护起来没有Map清爽。在实际项目中GET多个参数经常还伴随着校验注解比如Min、Max、NotBlank。测试时得把边界值也打进去mockMvc.perform(get(/api/user/list) .param(keyword, 张) .param(page, 0) // 非法page最小值应为1 .param(size, 10)) .andExpect(status().isBadRequest()); // 命中 Min(1) 时返回400这个断言思路在写测试时特别实用传合法参数断言业务结果正常传非法参数断言被校验拦截。一套测试用例就能把参数校验逻辑覆盖住这也是MockMvc相对Postman测试最有优势的地方——它能让校验规则的检验变成随代码提交的自动化资产。3.3 路径参数PathVariable场景的测试写法路径参数和Query参数在MockMvc里的写法完全不同。接口定义如果是GetMapping(/detail/{id}) public ResultUserVO detail(PathVariable(id) Long id) { return Result.success(userService.getById(id)); }测试的时候参数是直接写在URI模板里的mockMvc.perform(get(/api/user/detail/{id}, 1001L)) .andExpect(status().isOk()) .andExpect(jsonPath($.data.id).value(1001));这里有个细节需要注意get(String uriTemplate, Object... uriVariables)方法接受URI模板和可变参数框架会自动把1001L填充到{id}占位符里。你也可以直接用写死的字符串get(/api/user/detail/1001)效果一样。但如果你要测的是非法路径参数比如传入一个非数字mockMvc.perform(get(/api/user/detail/{id}, abc)) .andExpect(status().isBadRequest());这个场景下Spring MVC在参数转换阶段就会把String转Long的异常捕获住返回400。用URI模板方式的好处就在这里——传参和断言逻辑一目了然要测什么类型转换错误字符串一换就行。路径参数还有一个易错点占位符数量和可变参数数量必须一致。写了两个{}只传一个值或者传的值顺序搞错会把路径拼成一个完全不可达的URL测试结果就是404。排查这种问题的时候先别怀疑MockMvc去数一下占位符。3.4 GET测试中的中文乱码与编码断言中文参数是GET测试的重灾区。假设接口返回的是带中文的JSON数据比如{name: 张三}andExpect(jsonPath($.data.name).value(张三))挂掉报一个编码或者比较失败的错误大概率不是业务逻辑问题而是响应编码没对齐。Spring Boot默认的server.servlet.encoding配置是UTF-8正常情况没问题但如果你配了旧项目的StringHttpMessageConverter或者测试环境里CharacterEncodingFilter没有生效JSON返回的中文可能变成乱码。处理办法比较粗暴但很有效——显式声明响应格式和编码mockMvc.perform(get(/api/user/detail).param(id, 1001)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(jsonPath($.data.name).value(张三));如果jsonPath解析中文一直失败可以先把响应内容打出来看MockMvc提供了一个很实用的调试手段String responseBody mockMvc.perform(get(/api/user/detail).param(id, 1001)) .andReturn() .getResponse() .getContentAsString(StandardCharsets.UTF_8); System.out.println(responseBody);拿到原始响应字符串是乱码还是结构不对一眼就能定位。这一点在实际排查时非常省时间——很多问题其实是JSON结构层级和预想的不一样而不是中文编码问题。4. POST接口测试单参数、多参数与JSON Body4.1 单个POST参数RequestParam方式POST接口最容易被误解的地方是很多人一看到POST就默认要传JSON。实际上Spring MVC的POST支持好几种Content-Type最基础的其实是application/x-www-form-urlencoded用法和GET的Query参数几乎一致。假设接口定义是PostMapping(/search) public ResultListUserVO search(RequestParam(keyword) String keyword) { return Result.success(userService.search(keyword)); }MockMvc的写法mockMvc.perform(post(/api/user/search) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param(keyword, Java)) .andExpect(status().isOk()) .andExpect(jsonPath($.data).isArray());这里contentType(MediaType.APPLICATION_FORM_URLENCODED)可以加也可以不加但建议加上——因为如果你后面参数复杂了或者接口逻辑里对Content-Type有判断不加这个就可能导致分支走不对。Form表单方式传单个参数本质就是body里的keyvalue串param()方法往request的parameter map里塞数据Spring会把这部分数据解析成RequestParam。实际项目里单个参数的POST接口比较少更多是那种“表单里就一个关键字搜索框”的场景。测试代码很简单但有一条经验值得记住如果你的接口方法是public Result? search(String keyword)方法参数上没写RequestParam那么POST form和GET query都能匹配到参数MockMvc写法不变。而如果加了RequestParam那么参数来源受限传错方式直接400。测试的目的之一正是验证这些注解有没有按预期生效。4.2 多个POST参数表单提交方式的完整写法多个参数的form表单POST是最接近网页表单提交的场景。接口定义比如PostMapping(/add) public ResultLong add(RequestParam(name) String name, RequestParam(age) Integer age, RequestParam(email) String email) { return Result.success(userService.add(name, age, email)); }MockMvc可以这样测mockMvc.perform(post(/api/user/add) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param(name, 李四) .param(age, 25) .param(email, lisiexample.com)) .andExpect(status().isOk()) .andExpect(jsonPath($.data).isNumber());这个写法看起来和GET多参数一模一样区别在于请求方法变成了post()并且显式设置了表单的Content-Type。当你在测试里用了.param()以后MockMvc实际上会把参数构建成MultiValueMapString, String然后根据Content-Type决定是拼到URL上还是塞进请求body。有一点很重要对于POST表单提交如果你的参数是数组或者List比如接口接收RequestParam(tag) ListString tag那就在同一参数名下面add多个值mockMvc.perform(post(/api/user/batchTag) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param(id, 1001) .param(tag, 开发) .param(tag, 后端) .param(tag, Java)) .andExpect(status().isOk());这种用param()重复追加同一个key的写法在HTTP语义上就是提交多个同名表单字段Spring MVC能正确解析成List。实际测试时多参数场景建议把每个参数的边界值都列出来比如年龄传-1、邮箱传abc配合参数校验注解一起做断言。这样一套用例下来基本把接口的参数容错能力都覆盖了。4.3 JSON Body方式Controller层最常用的数据交换格式REST接口的POST绝大多数是JSON。接口定义PostMapping(/save) public ResultLong save(RequestBody Valid UserCreateDTO dto) { return Result.success(userService.save(dto)); }MockMvc传JSON需要两步设置Content-Type为application/json然后把JSON字符串塞进content()mockMvc.perform(post(/api/user/save) .contentType(MediaType.APPLICATION_JSON) .content({\name\:\王五\,\age\:30,\email\:\wangwuexample.com\}) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath($.code).value(200)) .andExpect(jsonPath($.data).value(1L));这里.content()接收的是字符串而且它不会自动帮你把对象序列化成JSON。很多人一开始会问“能不能传个对象进去”答案是不行——content()的重载方法只接受String和byte[]。所以要么手动拼JSON字符串要么用ObjectMapper序列化再传ObjectMapper objectMapper new ObjectMapper(); String json objectMapper.writeValueAsString(userCreateDTO); mockMvc.perform(post(/api/user/save) .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk());用ObjectMapper的好处是如果DTO字段特别多手写JSON字符串容易漏字段、写错类型名。尤其在测试代码里维护一堆字符串可读性和复用性都很差。我在实际项目里更推荐写一个工具方法专门做对象转JSON或者直接用Test方法内配合ObjectMapper转。反正核心思想就一个测试代码和业务代码一样也讲究可读性。4.4 多个POST参数之JSON嵌套与List字段多参数的POST接口如果参数不是一个扁平的DTO而是嵌套了另一个对象、或者带了一个对象数组这时候JSON body的优势才真正体现出来。比如PostMapping(/submitOrder) public ResultLong submitOrder(RequestBody OrderCreateDTO dto) { return Result.success(orderService.submit(dto)); }OrderCreateDTO里包含用户信息、收货地址和商品列表。构造测试数据时手写JSON会非常痛苦这时候用Map或DTO再序列化显著更合理MapString, Object address new HashMap(); address.put(province, 浙江省); address.put(city, 杭州市); address.put(detail, 文一西路100号); ListMapString, Object items new ArrayList(); MapString, Object item1 new HashMap(); item1.put(skuId, 10001L); item1.put(quantity, 2); items.add(item1); MapString, Object order new HashMap(); order.put(userId, 888L); order.put(address, address); order.put(items, items); String json objectMapper.writeValueAsString(order); mockMvc.perform(post(/api/order/submit) .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()) .andExpect(jsonPath($.data).isNumber());这种构造方式比拼接字符串安全得多而且ObjectMapper序列化Map时数字会自动按数字类型输出不会出现quantity: 2被写成quantity: 2的尴尬。测试通过jsonPath($.data.xxx)校验返回结果时也要注意返回结构里的字段类型——如果接口把Long序列化成了字符串Jackson的ToStringSerializer那么jsonPath(...).value(10001)就会匹配失败你得用value(10001)。这个细节我在项目里碰到过好几次排查起来特别容易头大但归根结底是接口序列化策略的问题不是MockMvc的问题。4.5 POST接口的文件上传与Multipart参数有些POST接口需要同时携带普通字段和文件比如用户头像上传PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file, RequestParam(dir) String dir) { return Result.success(fileService.upload(file, dir)); }MockMvc对multipart/form-data这种格式的支持很完整MockMultipartFile file new MockMultipartFile( file, test.png, MediaType.IMAGE_PNG_VALUE, fake-image-content.getBytes(StandardCharsets.UTF_8) ); mockMvc.perform(multipart(/api/user/upload) .file(file) .param(dir, avatar)) .andExpect(status().isOk()) .andExpect(jsonPath($.data).isNotEmpty());这里multipart(/api/user/upload)本身就是POST不用再显式调用post()。MockMultipartFile就是用来包装测试文件的构造参数分别是字段名、原始文件名、Content-Type和文件内容字节数组。如果你要模拟空文件传个空的byte[]就行要模拟大文件可以在字节数上做文章。一个常见的错误是文件字段名和接口定义的RequestParam(file)不一致。MockMvc不会替你纠正它会直接把“找不到文件字段”的信息以400的形式甩在脸上。排查这类问题第一件事就是核对字段名。还有一点如果你同时有文件和普通参数普通参数继续用param()它会自动作为Multipart参数提交。别用.content()去传普通参数Multipart请求里content()会被忽略参数根本到不了接口方法里。5. 响应结果的深度断言从状态码到JSONPath5.1 断言HTTP状态码和响应头写好请求之后重心就转移到响应断言上。最基本的判断是HTTP状态码mockMvc.perform(post(/api/user/save) .contentType(MediaType.APPLICATION_JSON) .content(json)) .andExpect(status().isOk()); // 200 // .andExpect(status().isBadRequest()); // 400 // .andExpect(status().isCreated()); // 201 // .andExpect(status().isInternalServerError()); // 500除了状态码响应头也是经常要校验的对象。比如接口需要返回自定义响应头.andExpect(header().string(X-Custom-Header, custom-value))对于带token鉴权的接口测试时可能还要断言Set-Cookie或者Authorization头.andExpect(header().exists(Authorization))响应内容类型也可以做精确校验.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))这里要注意如果接口返回的Content-Type是application/json;charsetUTF-8你直接用MediaType.APPLICATION_JSON_VALUE不带charset去匹配MockMvc内部对Content-Type的比较是严格模式还是宽松模式取决于版本。实际碰到过的情况是Spring Boot 2.x中content().contentType()的比较是支持带charset的但如果服务端配置了自定义的MediaType就很容易出现断言意外的失败。稳妥的做法是用com.jayway.jsonpath去解析JSON而不是死磕Content-Type的字符串匹配。5.2 jsonPath用法与常见的JSON结构断言我们可以利用jsonPath对返回的JSON做非常深层次的校验。假设接口返回了一串分页数据{ code: 200, message: SUCCESS, data: { total: 15, records: [ {id: 1, name: 张三}, {id: 2, name: 李四} ], hasNext: true } }对应的断言可以这样写.andExpect(jsonPath($.code).value(200)) .andExpect(jsonPath($.data.total).value(15)) .andExpect(jsonPath($.data.records).isArray()) .andExpect(jsonPath($.data.records[0].name).value(张三)) .andExpect(jsonPath($.data.records.length()).value(2)) .andExpect(jsonPath($.data.hasNext).value(true));jsonPath的表达式能力足够覆盖90%的接口测试需求表达式含义示例$.field取顶层字段$.code$.a.b.c取嵌套层级字段$.data.total$.arr[0]取数组第一个元素$.data.records[0]$.arr.length()取数组长度$.data.records.length()$.arr[*].name取出数组所有对象的name字段$.data.records[*].name$.hasNext取布尔字段$.data.hasNext.name过滤条件中用一般配合filter使用有一种很常见的断言需求是“数组里是否包含某个值”可以直接配合Hamcrest的集合匹配器.andExpect(jsonPath($.data.records[*].name, hasItem(李四)))这里用到了hasItem它来自org.hamcrest.Matchers。需要静态导入import static org.hamcrest.Matchers.hasItem;这行断言的意思是records数组里面所有对象的name字段组成的列表中必须包含“李四”。对于接口查询列表这类功能这是非常硬核的验证。5.3 andReturn()与手动解析响应体有些测试场景不是只用andExpect链式断言能覆盖的比如需要拿到响应的body去做复杂的JSON解析、校验某个字段的唯一性、或者要把响应数据作为上下文传给下一个测试步骤。这时候用andReturn()MvcResult result mockMvc.perform(get(/api/user/list) .param(keyword, 张)) .andExpect(status().isOk()) .andReturn(); String responseBody result.getResponse().getContentAsString(StandardCharsets.UTF_8); ObjectMapper mapper new ObjectMapper(); JsonNode root mapper.readTree(responseBody); JsonNode records root.path(data).path(records); for (JsonNode record : records) { System.out.println(record.get(name).asText()); }实际项目里我见过很多人把andReturn()和andExpect()混合使用其实两者之间的关系很简单andExpect本质是对MvcResult做断言多了andReturn之后你既保留了链式断言又拿到了完整的响应对象做后续操作。这种方式特别适合“先断言接口能返回再拿数据继续测试下一个接口”的场景比如先创建订单拿到订单号再用这个订单号去查详情MvcResult createResult mockMvc.perform(post(/api/order/save) .contentType(MediaType.APPLICATION_JSON) .content(orderJson)) .andExpect(status().isOk()) .andReturn(); long orderId mapper.readTree( createResult.getResponse().getContentAsString(StandardCharsets.UTF_8)) .path(data).asLong(); mockMvc.perform(get(/api/order/detail/{id}, orderId)) .andExpect(status().isOk()) .andExpect(jsonPath($.data.id).value(orderId));这种“串联测试”让MockMvc的玩法又深了一层也是接口回归测试里非常有用的模式。你不需要停下来用Postman手动去复制的路径参数MockMvc已经把流程自动化了。6. 常见问题与排查技巧实录6.1 问题速查表我踩过的那些坑MockMvc用久了会遇到一些很有规律的问题。这里整理一个排查速查表异常/现象最可能的原因解决办法org.springframework.web.util.NestedServletException: Request processing failedController或Service抛出未捕获异常查Controller内部代码看是否ControllerAdvice没有覆盖该异常java.lang.AssertionError: Status expected:400 but was:200参数校验注解未生效或参数传错了都没触发校验检查Controller类是否加了ValidatedRequestParam上有没有校验注解404URL路径拼写错误或RequestMapping路径不对直接从Controller里复制路径不要手敲400且报MissingServletRequestParameterExceptionGET/POST参数名和RequestParam里的名字不一致核对参数名特别是拼写和大小写Content type not supportedPOST接口期望JSON但测试里没设置Content-Type加上.contentType(MediaType.APPLICATION_JSON)jsonPath断言报错No results for path: $[data][name]返回JSON结构里根本没有这个字段先andReturn()把响应体打出来检查实际结构中文乱码响应编码或请求参数编码问题用getContentAsString(StandardCharsets.UTF_8)读取检查编码过滤器BeanInstantiationExceptionWebMvcTest里没Mock掉Service导致上下文加载失败给依赖的Service加MockBeanMockMvc为null测试类没有加AutoConfigureMockMvc或WebMvcTest检查测试类上的注解确保Spring上下文装载了MockMvc仔细看这张表其实大部分问题集中在三个方向一是参数名对不上二是Content-Type不对三是JSON结构预期和实际不一致。这三个方向占了MockMvc排错的八成以上所以排查思路基本是“先用andReturn()把响应打出来看再对照Controller方法的参数定义和注解”两步就能定位。6.2 三个隐蔽的坑新手基本都会踩第一个坑是**WebMvcTest下没有Mock Service**。很多新手照着一个教程写测试类里用了WebMvcTest(SomeController.class)但Controller里又注入了SomeService。启动测试直接报BeanInstantiationException或者NoSuchBeanDefinitionException原因就是切片测试不加载Service。解决办法是给Service加MockBeanMockBean private SomeService someService;如果是Spring Boot 3.4开始MockBean被标记为弃用新推荐的是MockitoBean。如果你用的是Boot 3.4直接换成org.springframework.test.context.bean.override.mockito.MockitoBean即可。第二个坑是**jsonPath().value()的类型比较**。接口返回的JSON数字在Jackson默认配置下小整数会被序列化成IntNode或者LongNode但如果你在DTO里把数字声明成了String又配了Jackson的ToStringSerializer那么前端拿到的就是一个字符串。测试里写value(1001)必然失败。这不是MockMvc的错而是测试预期没有和实际的序列化策略保持一致。应对办法是发现数字断言失败第一时间去查DTO字段类型和Jackson全局配置别在MockMvc的写法上反复折腾。第三个坑是过滤器或拦截器干扰了MockMvc的结果。比如项目里有一个登录拦截器所有接口都得带token测试时直接请求就返回401Controller根本没进到。WebMvcTest下部分过滤器会被自动加载如果你的过滤器是通过FilterRegistrationBean注册的在测试中默认可能不加载但拦截器是通过WebMvcConfigurer注册的一定会加载。这时候要么在测试里排除拦截器通过WebMvcTest的excludeFilters属性或者排除配置类要么干脆在测试请求里带上token。我更推荐带上token这样测试能覆盖真实场景能顺便验证拦截器的行为。6.3 调试技巧怎么把MockMvc测试失败的信息看得更清楚排查MockMvc问题有一个必杀技在perform之后不直接断言而是先打印请求和响应信息。MockHttpServletRequestBuilder requestBuilder get(/api/user/detail) .param(id, 1001); mockMvc.perform(requestBuilder) .andDo(print()) // 这行会把请求和响应完整打印出来 .andExpect(status().isOk());andDo(print())是MockMvc最实用的调试API之一。它会输出请求头、请求参数、响应状态、响应头、响应body。当你写了一个复杂的POST接口测试每次都失败但不知道失败原因时先加上这一句日志里就能看到完整的MockHttpServletRequest和MockHttpServletResponse的信息问题基本就水落石出了。我个人的调试习惯是先用andDo(print())跑一次看到响应body和状态码。看响应里的JSON结构是不是和预期的一样。如果结构不对检查Controller代码和DTO定义。如果结构对但断言失败检查jsonPath表达式和类型匹配。最后删掉andDo(print())保留干净的断言链。这个流程效率非常高能避免大多数无头苍蝇式的排查。7. 关于MockMvc我个人的一些实操体会写到这里其实MockMvc的核心用法都覆盖得差不多了。回头看MockMvc真正强大的地方不只是能模拟GET和POST请求而是它把接口测试变成了一种可以随代码提交、随CI运行的标准化动作。以前用Postman测接口测完了就完了没有留下任何可回归的资产。而MockMvc测试写在src/test/java里每次构建都执行任何一次改动让接口行为变了测试立刻报警这个价值对我来说比“能跑通一个接口”重要得多。我在实际项目中的做法是核心业务接口必须配有MockMvc测试而且测试里不光覆盖正常路径还要覆盖参数校验失败、鉴权失败、资源不存在这些异常分支。这套东西维护起来成本并不高但每次重构Controller参数、调整响应结构的时候它都能帮我省下大量手工回归的时间。特别是多人协作的项目里接口改动常常有连锁反应——你以为改的是内部实现结果把接口返回值结构改了前端感知不到等联调时才发现。有了MockMvc测试的守护这种问题在提交代码前就暴露了。如果你刚开始接触MockMvc我建议从最简单的GET接口单参数测试入手跑通了之后依次尝试多参数、POST表单、JSON body、文件上传、异常断言。练完这一圈你对Spring MVC参数绑定机制的理解会明显上一个台阶——因为MockMvc的每一个测试用例本质上都是在对你写的接口定义做验证它会强制你去审视参数注解、校验规则、响应结构这些平时容易忽略的细节。后面再遇到“为什么Postman能通但测试挂了”之类的问题你的排查思路就会清晰很多先看Context-Type再看参数名再看响应结构三板斧下来基本都能解决。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进