Skip to content

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 容器中的过滤器链行为:

  1. 顺序执行:按照注册顺序依次执行过滤器
  2. 链式调用:每个过滤器都可以决定是否继续执行下一个过滤器
  3. 最终委托:最后一个过滤器将请求委托给 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 层测试能力:

  1. 完整性:能够测试包含 Filter 的完整请求处理流程
  2. 灵活性:支持注册多个 Filter 并控制执行顺序
  3. 真实性:模拟真实 Servlet 容器的 Filter 链行为
  4. 可控性:在测试环境中精确控制 Filter 的行为

通过合理使用 Filter 注册功能,我们可以编写更加全面和可靠的 Web 层测试,确保应用在生产环境中的正确行为。

NOTE

记住,好的测试不仅要覆盖正常流程,还要验证异常情况和边界条件。Filter 注册功能让这一切变得更加容易!