Spring MVC HandlerInterceptor 测试方法
1. 概述
Spring MVC 中的 HandlerInterceptor 允许我们在控制器执行前后拦截请求。它适用于日志记录、认证、功能开关等横切关注点。
测试 Spring MVC HandlerInterceptor 可能比较棘手,因为它们被嵌入到请求生命周期中。本文将探讨如何使用 @WebMvcTest 和 MockMvc 来测试拦截器,确保拦截器逻辑正确运行,而无需启动完整的应用程序。
2. HandlerInterceptor 接口
HandlerInterceptor 允许我们在控制器处理请求之前和之后执行逻辑。它运行在 Spring MVC 层内,能够访问处理程序的元数据。
Spring MVC 的 HandlerInterceptor 接口定义了三个生命周期方法:
preHandle()—— 在控制器方法执行前运行postHandle()—— 在控制器完成执行后、视图生成前运行afterCompletion()—— 在整个请求完成后运行,无论是否出现异常
3. 应用程序设置
假设我们有一个计算配送费用的订单处理应用程序。我们希望逐步推出新的定价算法,而无需重新部署应用程序。为此,我们配置一个分发百分比,决定多少请求使用新的计算方式(v2),其余请求回退到原有方式(v1)。我们将使用 HandlerInterceptor 在控制器执行前做出路由决策,使控制器不包含功能开关逻辑。
首先定义 FeatureFlagService 接口,它代表一个提供当前分发百分比的外部服务:
实现细节在此不相关,因此在测试中我们会模拟该服务。
3.1 拦截器
接下来创建 DeliveryChargeInterceptor。它查询分发百分比,生成一个随机值,当随机值低于分发阈值时路由到 v2:
handler instanceof HandlerMethod 检查确保我们的逻辑仅应用于控制器方法,跳过静态资源请求。另外需要注意的是,拦截器不执行业务逻辑,它仅仅做出路由决策并通过请求属性传递。
3.2 控制器
接下来创建读取该属性并委托给相应版本的控制器:
3.3 注册拦截器
最后,通过 WebMvcConfigurer 注册拦截器,并将其作用域限定在配送费用路径:
addPathPatterns() 方法确保只有匹配该路径的请求才会触发拦截器,其他端点不受影响。
4. 测试 Spring MVC HandlerInterceptor
现在,我们使用 @WebMvcTest 验证拦截器行为。该注解仅加载 Web 层,无需启动完整应用上下文。它包含控制器、拦截器及其配置。我们使用 @MockBean 为服务接口提供模拟实现:
值得注意的是,@WebMvcTest 会自动拾取应用上下文中所有的 WebMvcConfigurer Bean。因为我们的 WebMvcConfig 注册了拦截器,所以拦截器在测试中处于激活状态。这意味着我们能够测试从拦截器到控制器的完整请求流程,而无需额外配置。
首先验证当分发为 0 时所有请求都使用 v1:
分发为 0 时,拦截器中的随机检查始终返回 false,因此控制器始终调用 calculateV1()。
接下来验证当分发为 100% 时所有请求都使用 v2:
100% 分发时,随机值始终小于阈值,因此每个请求都路由到 calculateV2()。
最后验证 50% 分发会产生两种版本的混合使用。我们发送 20 个请求,断言 v1 和 v2 都接收到流量:
该测试验证拦截器根据配置的百分比在两种版本之间分配流量。
5. 结论
本文探讨了如何使用 @WebMvcTest 和 MockMvc 测试 Spring MVC HandlerInterceptor。这种方法很有用,因为 @WebMvcTest 仅加载 Web 层,使测试速度更快。它会自动拾取通过 WebMvcConfigurer 注册的拦截器,因此我们无需启动完整应用上下文即可验证完整的请求流程。
然而,@WebMvcTest 仅隔离测试 Web 层,服务或仓库的集成问题不会在此暴露。对于需要完整应用上下文的场景,可以使用 @SpringBootTest 与 MockMvc。此外,对于不依赖 Spring 请求处理的简单拦截器逻辑,使用 MockHttpServletRequest 进行单元测试能提供更快的反馈。
与往常一样,本文的源代码可在 GitHub 上获取。