Appearance
Spring Boot MockMvc Filter 注册详解 ⚙️
概述
在 Spring Boot 测试中,MockMvc 是一个强大的工具,它允许我们在不启动完整服务器的情况下测试 Web 层。而 Filter 注册功能则让我们能够在测试环境中模拟真实的 Servlet Filter 行为,确保测试的完整性和准确性。
NOTE
MockMvc Filter 注册是 Spring Test 框架的重要特性,它让我们能够在单元测试中完整地模拟 Web 请求的处理流程。
为什么需要 Filter 注册? 🤔
没有 Filter 注册的痛点
想象一下,如果我们的 Web 应用中有以下场景:
- 字符编码处理:需要确保请求和响应的字符编码正确
- 安全认证:需要验证用户身份和权限
- 请求日志记录:需要记录每个请求的详细信息
- CORS 处理:需要处理跨域请求
如果在测试中无法注册这些 Filter,我们就无法:
- 验证 Filter 的逻辑是否正确
- 测试 Filter 与 Controller 的交互
- 确保完整的请求处理流程
Filter 注册解决的核心问题
核心概念解析 💡
MockFilterChain 的工作原理
MockFilterChain 是 Spring Test 提供的过滤器链实现,它模拟了真实 Servlet 容器中的过滤器链行为:
- 顺序执行:按照注册顺序依次执行过滤器
- 链式调用:每个过滤器都可以决定是否继续执行下一个过滤器
- 最终委托:最后一个过滤器将请求委托给 DispatcherServlet
IMPORTANT
理解过滤器链的执行顺序对于正确配置测试环境至关重要。
实际应用示例 🛠️
基础 Filter 注册
kotlin
@WebMvcTest(PersonController::class)
class PersonControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Test
fun `测试带字符编码过滤器的请求`() {
// 创建带有字符编码过滤器的 MockMvc
val mockMvcWithFilter = MockMvcBuilders
.standaloneSetup(PersonController())
.addFilters(CharacterEncodingFilter("UTF-8", true))
.build()
mockMvcWithFilter.perform(
post("/persons")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"name": "张三", "age": 25}""")
)
.andExpect(status().isOk)
.andExpect(content().encoding("UTF-8"))
}
}java
@WebMvcTest(PersonController.class)
public class PersonControllerTest {
@Test
public void testWithCharacterEncodingFilter() throws Exception {
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new PersonController())
.addFilters(new CharacterEncodingFilter("UTF-8", true))
.build();
mockMvc.perform(post("/persons")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\": \"张三\", \"age\": 25}"))
.andExpect(status().isOk())
.andExpect(content().encoding("UTF-8"));
}
}多个 Filter 注册示例
kotlin
@Component
class RequestLoggingFilter : Filter {
private val logger = LoggerFactory.getLogger(RequestLoggingFilter::class.java)
override fun doFilter(
request: ServletRequest,
response: ServletResponse,
chain: FilterChain
) {
val httpRequest = request as HttpServletRequest
logger.info("请求路径: ${httpRequest.requestURI}")
val startTime = System.currentTimeMillis()
chain.doFilter(request, response)
val endTime = System.currentTimeMillis()
logger.info("请求耗时: ${endTime - startTime}ms")
}
}
@Component
class SecurityFilter : Filter {
override fun doFilter(
request: ServletRequest,
response: ServletResponse,
chain: FilterChain
) {
val httpRequest = request as HttpServletRequest
val token = httpRequest.getHeader("Authorization")
if (token.isNullOrBlank()) {
(response as HttpServletResponse).status = 401
return
}
chain.doFilter(request, response)
}
}完整的测试配置
kotlin
@WebMvcTest(PersonController::class)
class PersonControllerFilterTest {
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setup() {
mockMvc = MockMvcBuilders
.standaloneSetup(PersonController())
.addFilters(
CharacterEncodingFilter("UTF-8", true),
RequestLoggingFilter(),
SecurityFilter()
)
.build()
}
@Test
fun `测试无认证头的请求被拒绝`() {
mockMvc.perform(
get("/persons/1")
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status().isUnauthorized)
}
@Test
fun `测试有效认证头的请求成功`() {
mockMvc.perform(
get("/persons/1")
.header("Authorization", "Bearer valid-token")
.contentType(MediaType.APPLICATION_JSON)
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.name").exists())
}
}高级应用场景 🚀
条件性 Filter 注册
kotlin
@TestConfiguration
class TestFilterConfiguration {
@Bean
@ConditionalOnProperty(name = "test.security.enabled", havingValue = "true")
fun securityFilter(): SecurityFilter = SecurityFilter()
@Bean
@Profile("test")
fun debugFilter(): Filter = object : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
println("Debug: 处理请求 ${(request as HttpServletRequest).requestURI}")
chain.doFilter(request, response)
}
}
}Filter 执行顺序测试
kotlin
@Test
fun `验证过滤器执行顺序`() {
val executionOrder = mutableListOf<String>()
val filter1 = object : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
executionOrder.add("Filter1-Before")
chain.doFilter(request, response)
executionOrder.add("Filter1-After")
}
}
val filter2 = object : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
executionOrder.add("Filter2-Before")
chain.doFilter(request, response)
executionOrder.add("Filter2-After")
}
}
val mockMvc = MockMvcBuilders
.standaloneSetup(PersonController())
.addFilters(filter1, filter2)
.build()
mockMvc.perform(get("/persons/1"))
.andExpect(status().isOk)
// 验证执行顺序
assertThat(executionOrder).containsExactly(
"Filter1-Before",
"Filter2-Before",
"Filter2-After",
"Filter1-After"
)
}最佳实践与注意事项 ⭐
1. Filter 的生命周期管理
TIP
在测试中,Filter 的生命周期由 MockMvc 管理,无需手动初始化或销毁。
2. 性能考虑
WARNING
过多的 Filter 可能会影响测试性能,建议只注册测试场景必需的 Filter。
3. Filter 与 Spring Security 集成
kotlin
@Test
fun `测试 Spring Security Filter 集成`() {
val mockMvc = MockMvcBuilders
.standaloneSetup(PersonController())
.apply(springSecurity())
.addFilters(CharacterEncodingFilter("UTF-8", true))
.build()
mockMvc.perform(
get("/persons/1")
.with(user("testuser").roles("USER"))
)
.andExpect(status().isOk)
}4. 异常处理测试
kotlin
@Test
fun `测试 Filter 中的异常处理`() {
val exceptionFilter = object : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
throw RuntimeException("Filter 异常")
}
}
val mockMvc = MockMvcBuilders
.standaloneSetup(PersonController())
.addFilters(exceptionFilter)
.build()
assertThrows<RuntimeException> {
mockMvc.perform(get("/persons/1"))
}
}常见问题与解决方案 ❓
问题1:Filter 未按预期执行
解决方案
kotlin
// 确保 Filter 正确注册
@Test
fun `调试 Filter 执行`() {
val debugFilter = object : Filter {
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
println("Filter 被执行了!")
chain.doFilter(request, response)
}
}
val mockMvc = MockMvcBuilders
.standaloneSetup(PersonController())
.addFilters(debugFilter)
.build()
mockMvc.perform(get("/persons/1"))
}问题2:Filter 顺序错误
WARNING
Filter 的注册顺序决定了执行顺序,请确保按照业务逻辑需求正确排序。
总结 🎉
MockMvc 的 Filter 注册功能为我们提供了完整的 Web 层测试能力:
- 完整性:能够测试包含 Filter 的完整请求处理流程
- 灵活性:支持注册多个 Filter 并控制执行顺序
- 真实性:模拟真实 Servlet 容器的 Filter 链行为
- 可控性:在测试环境中精确控制 Filter 的行为
通过合理使用 Filter 注册功能,我们可以编写更加全面和可靠的 Web 层测试,确保应用在生产环境中的正确行为。
NOTE
记住,好的测试不仅要覆盖正常流程,还要验证异常情况和边界条件。Filter 注册功能让这一切变得更加容易!