Spring Boot接口测试实战:MockMvc常用写法与坑总结

发布时间:2026/9/26 11:55:16
Spring Boot接口测试实战:MockMvc常用写法与坑总结 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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependencyspring-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 ResultUserVO 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)会把参数拼成QueryStringSpring MVC的RequestParam就能拿到。注意param的value是String类型框架会自动做类型转换所以传数字时写字符串就可以。.andExpect()里用的是一连串的静态方法一般都会使用MockMvcRequestBuilders和MockMvcResultMatchers这两个类很多人会采取静态导入简化代码不过为了可读性项目里可以约定保留前缀或全部静态导包方式保持一致即可。3.2 GET接口单个参数的各种写法实际项目中GET接口参数不止一种有些用RequestParam有些用PathVariable还有些直接绑定一个DTO对象。三种写法分别对应不同的MockMvc请求方式。PathVariable风格的接口GetMapping(/detail/{id}) public ResultUserVO 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 ResultPageResultUserVO 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里真的包含英文逗号。所以当某个参数业务上需要传多个值给ListLong接收的时候就没法用单个param了得用param(ids, 1,2,3)或者连写多个param参数两种写法框架都能正确绑定。多次实验下来多参数的GET测试最需要注意的是required false的字段。测试用例里应该专门设计一个“不传可选参数”的用例确保接口在缺少可选参数时不会报500。比如上面这个接口keyword和status都是可选的那至少要跑一次只传分页参数的情况。3.4 GET接口对象绑定的测试Spring MVC支持把QueryString参数直接绑定到一个POJO对象上比如GetMapping(/search) public ResultListUserVO 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-urlencodedPOST请求最常见的两种数据格式是表单提交和JSON。先看表单场景PostMapping(/save) public ResultLong 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, lisiexample.com)) .andExpect(status().isOk()) .andExpect(jsonPath($.code).value(200));注意有个小坑当contentType是表单格式的时候MockMvc会自动把param放到请求体里并编码成name李四age28的形式不用手动去拼这个字符串。如果手动用.content(name李四age28)记得对中文做URL编码不然容易乱码。我在早期就吃过这个亏手动拼body传中文结果接口收到的全是乱码。4.2 POST JSON请求体的测试现在的项目更流行前后端分离接口大部分用JSON作为数据交换格式。场景变成这样PostMapping(/add) public ResultLong 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(wangwuexample.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 ResultBoolean batchCreate(RequestBody UserBatchCreateDTO batchDTO) { return Result.success(userService.batchCreate(batchDTO)); }UserBatchCreateDTO除了基本字段还有列表public class UserBatchCreateDTO { private Long groupId; private ListUserAddDTO 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(zhaoliuexample.com); UserAddDTO user2 new UserAddDTO(); user2.setName(孙七); user2.setAge(26); user2.setEmail(sunqiexample.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 ResultString 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(ListHttpMessageConverter? 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加charsetUTF-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, MapString, 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测试和单元测试不同它走的是真实接口链路很可能干到数据库里的数据。同一条测试跑第二次可能就报唯一约束冲突或者列表查询的结果数量不对。解决方案我自己常用的有以下几种测试前置方法里构造数据测试后置方法里清空数据尤其是针对主键、唯一索引字段的数据使用事务回滚思路比如在测试方法上配合TransactionalSpring会帮你回滚测试内产生的事务但要注意有些框架如部分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 ResultLong 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的参数解析流程大多数问题都能找到根因。