测试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 注册了拦截器,因此在测试期间它处于激活状态。这意味着我们无需额外配置即可测试从拦截器到控制器的完整请求流程。
首先验证当上线百分比为零时所有请求都使用 v1:
上线百分比为零时,拦截器中的随机检查始终为 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 上获取。