你还在用 MockMvc 硬怼一切?那为啥你的集成测试越写越像在“写脚本”?

举报
bug菌 发表于 2026/01/13 11:35:24 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
  
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
  
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

1) 传统 MockMvc 的局限性(不是不好,是“视角”有限)

Spring 官方对 MockMvc 的定位很明确:它测试的是 Spring MVC 的请求处理流程,但使用的是 mock 的 request/response,而不是跑一个真实服务器。
这带来几个常见局限(说人话版):

1.1 更像“框架内部测试”,而不是“客户端视角”

MockMvc 的断言链条通常围绕 ResultActions / ResultMatcher,你会更容易去断言“某个 handler、某个 view、某个 model”,而不是像真实客户端那样按 HTTP 体验去写测试。
当你开始做 流式响应、SSE、复杂 header/cookie 行为 时,MockMvc 的“内部视角”会让你写得很别扭(当然不是不能做,是不顺手😅)。

1.2 容易写出“过度耦合实现细节”的测试

MockMvc 太容易让人去断言细节(比如具体用哪个 controller method、某个 attribute 是否存在)。一旦你重构控制器/路由结构,测试就会像多米诺骨牌一样倒一片。

1.3 MockMvc 仍然“不是端到端”

它不跑真实网络,不经过真实容器/端口。对某些“只在真实容器环境才触发”的问题(例如反向代理、真实网络超时、某些容器 filter 链顺序差异)覆盖不足。
不过这点要公平:WebTestClient 绑定 mock server 时也一样不是真端到端,端到端要连到真实服务器地址(后面会讲)。

2) WebTestClient:不仅用于 WebFlux,也能用于 Servlet 栈(Spring MVC)

这里是关键点:WebTestClient 并不等于 WebFlux 专属。Spring Framework 官方文档明确写了:WebTestClient 可以用于端到端 HTTP 测试,也可以在没有运行服务器的情况下测试 Spring MVC 或 Spring WebFlux,通过 mock server request/response 来处理请求。

更具体一点:在 Spring MVC(Servlet 栈)里,WebTestClient 可以通过 MockMvcWebTestClient 把 MockMvc “插”进去当服务端

2.1 你会爱上它的原因:测试写起来更像“真实客户端”

WebTestClient 本质是一个测试用的 HTTP 客户端外观(facade),包装 WebClient,并提供一套响应验证 API。
它的断言写起来更像“我发请求 → 我验响应”,可读性通常比 MockMvc 更强。

2.2 在 Servlet 栈里用 WebTestClient 的两种姿势

A) 绑定到 Controller(更轻量:类似 MockMvc standalone)

Spring 官方给的 Servlet 栈示例:
MockMvcWebTestClient.bindToController(...) 会委托 StandaloneMockMvcBuilder 创建 MockMvc,再由 WebTestClient 作为客户端去测。

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;

class UserControllerWebTestClientStandaloneTests {

  WebTestClient client;

  @BeforeEach
  void setUp() {
    client = MockMvcWebTestClient
        .bindToController(new UserController())
        .build();
  }

  @Test
  void getUser_shouldReturn200() {
    client.get().uri("/users/1")
        .exchange()
        .expectStatus().isOk()
        .expectHeader().contentType("application/json");
  }
}

B) 绑定到 ApplicationContext(更接近集成测试:含 MVC 基础设施、拦截器等)

Spring 文档同样明确:WebTestClient 可以绑定到 ApplicationContext,在没有运行服务器的情况下,用 mock request/response 测 Spring MVC。

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.client.MockMvcWebTestClient;
import org.springframework.web.context.WebApplicationContext;

@SpringBootTest
class UserControllerWebTestClientContextTests {

  @Autowired WebApplicationContext wac;
  WebTestClient client;

  @BeforeEach
  void setUp() {
    client = MockMvcWebTestClient.bindToApplicationContext(wac).build();
  }

  @Test
  void listUsers_shouldReturnJsonArray() {
    client.get().uri("/users")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
        .jsonPath("$.length()").isEqualTo(3);
  }
}

小提醒:这仍是 mock server(不是跑端口)。想做端到端,需要 WebTestClient.bindToServer().baseUrl("http://localhost:8080")... 这种连真实地址的方式(WebTestClient 本来就支持端到端 HTTP 测试)。

3) REST API 断言技巧:JsonPath 高级用法(别只会 $.id 🙃)

3.1 MockMvc:传统 jsonPath() 之外,推荐看看 AssertJ 的 bodyJson()

Spring Framework 在测试章节里提供了 MockMvc 的 AssertJ 风格断言:bodyJson() + extractingPath(),可以把 JsonPath 的结果转成专门的断言对象(Map、List、String、Number 等),甚至可以转换成你的类型再断言。

示例:断言对象结构 + 转换类型

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;

@Test
void jsonPath_advanced_assertj(MockMvcTester mockMvc) {
  assertThat(mockMvc.get().uri("/family"))
      .bodyJson()
      .extractingPath("$.members[0]")
      .asMap()
      .containsEntry("name", "Homer");
}

这个写法的好处是:断言更像在断言“数据结构”而不是字符串,可读性强很多。

3.2 WebTestClient:expectBody().jsonPath(...) 走起

WebTestClient 自带 JSONPath 断言链,写起来很顺:

client.get().uri("/users/1")
  .exchange()
  .expectStatus().isOk()
  .expectBody()
  .jsonPath("$.id").isEqualTo(1)
  .jsonPath("$.roles[0]").isEqualTo("ADMIN")
  .jsonPath("$.profile.address.city").isEqualTo("Shanghai");

3.3 JsonPath “更高级但很实用”的几个套路(两边都适用)

  • 断言数组长度$.items.length()$.length()(取决于你的返回结构)

  • 断言存在性jsonPath("$.token").exists()(MockMvc 有,WebTestClient 也有类似 exists/notEmpty)

  • 断言数值范围:先取出来转型,再做断言(AssertJ 风格更舒服)

  • 断言数组某元素字段$.items[?(@.id==123)].name(JsonPath 过滤表达式)

    这类过滤表达式对“列表里找某对象”非常省心,能少写一堆手工 parse。

4) 安全接口测试:@WithMockUser 与模拟 OAuth2 登录用户

4.1 @WithMockUser:最快的“我先假装登录了”✅

Spring Security 官方 API 文档解释得很清楚:@WithMockUser 会创建一个空的 SecurityContext,并放入一个 UsernamePasswordAuthenticationToken,包含 username、roles/authorities 等。

示例:MockMvc + @WithMockUser

import org.junit.jupiter.api.Test;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

class SecureApiMockMvcTests {

  MockMvc mvc;

  @Test
  @WithMockUser(username = "nadine", roles = {"ADMIN"})
  void adminEndpoint_shouldPass() throws Exception {
    mvc.perform(get("/admin/metrics"))
        .andExpect(status().isOk());
  }

  @Test
  void adminEndpoint_withoutUser_should401() throws Exception {
    mvc.perform(get("/admin/metrics"))
        .andExpect(status().isUnauthorized());
  }
}

什么时候用它最合适?

  • 你的接口只关心“已认证/角色”,不关心 OAuth2 的 token/claims 细节。
  • 你要快速覆盖一堆权限矩阵测试(非常适合)。

4.2 需要 OAuth2 / OIDC 细节?用 Spring Security 的测试支持(oauth2Login/oidcLogin)

如果你的控制器/业务逻辑真的绑定到了 OAuth2User / OidcUser 的属性(比如从 claims 里取 email、tenantId),那 @WithMockUser 就不够“像真登录”了。

Spring Security Reference 专门有一节 “Testing OAuth 2.0”,说明可以用测试支持来模拟 OAuth2 登录流,避免自己 mock 授权流程。

示例:MockMvc 模拟 OIDC/OAuth2 登录(思路展示)

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

@Test
void endpoint_withOidcLogin_shouldOk() throws Exception {
  mvc.perform(get("/me").with(oidcLogin()))
     .andExpect(status().isOk());
}

这类 RequestPostProcessor 的体系也在 Spring Security 测试文档中被系统化说明。

一句话对比(但不敷衍🙂)

  • MockMvc:更贴 MVC 内部处理流程;适合写“框架级集成测试”(controller + filter + handler 等),但测试表达有时更像“验证实现细节”。
  • WebTestClient:更贴客户端视角;在 WebFlux 和 Servlet 栈都能用(Servlet 栈可通过 MockMvcWebTestClient 绑定 MockMvc/Context)。
  • 安全测试:简单角色就 @WithMockUser;需要 OAuth2/OIDC 细节就用 oauth2Login/oidcLogin 这类测试支持。

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee:https://gitee.com/bugjun01/SpringBoot-demo 开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

🫵 Who am I?

我是 bug菌:

  • 热活跃于 CSDN:https://blog.csdn.net/weixin_43970743 | 掘金:https://juejin.cn/user/695333581765240 | InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish | 51CTO:https://blog.51cto.com/u_15700751 | 华为云:https://bbs.huaweicloud.cn/community/usersnew/id_1582617489455371 | 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke | 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles 等技术社区;
  • CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
  • 掘金、InfoQ、51CTO 等平台签约及优质作者;
  • 全网粉丝累计 30w+

更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。

- End -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。