Skip to content

MockMvc Setup Features:让测试配置更优雅 🎯

什么是 MockMvc Setup Features?

MockMvc Setup Features 是 Spring Test 框架中的一套配置功能,它允许我们在创建 MockMvc 实例时预设一些通用的请求和响应行为。这就像是为你的测试环境设置一套"默认规则",让每个测试都能自动遵循这些规则。

NOTE

MockMvc Setup Features 的核心理念是"约定优于配置"——通过预设常用的测试配置,减少重复代码,提高测试的一致性和可维护性。

为什么需要 Setup Features?

解决的核心痛点

在没有 Setup Features 之前,我们的测试代码可能是这样的:

kotlin
@Test
fun testGetUser() {
    mockMvc.perform(
        get("/api/users/1")
            .accept(MediaType.APPLICATION_JSON) 
    )
    .andExpect(status().isOk()) 
    .andExpect(content().contentType("application/json;charset=UTF-8")) 
}

@Test
fun testCreateUser() {
    mockMvc.perform(
        post("/api/users")
            .accept(MediaType.APPLICATION_JSON) 
            .contentType(MediaType.APPLICATION_JSON)
            .content("""{"name":"张三"}""")
    )
    .andExpect(status().isOk()) 
    .andExpect(content().contentType("application/json;charset=UTF-8")) 
}
kotlin
// 在测试类初始化时配置一次
@BeforeEach
fun setup() {
    mockMvc = MockMvcBuilders.standaloneSetup(UserController())
        .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON)) 
        .alwaysExpect(status().isOk()) 
        .alwaysExpect(content().contentType("application/json;charset=UTF-8")) 
        .build()
}

@Test
fun testGetUser() {
    // 自动应用默认配置,代码更简洁
    mockMvc.perform(get("/api/users/1"))
        .andExpect(jsonPath("$.name").value("张三"))
}

TIP

可以看到,使用 Setup Features 后,我们的测试代码变得更加简洁,重复的配置被消除了,测试的焦点更加集中在业务逻辑的验证上。

核心功能详解

1. 默认请求配置 (defaultRequest)

defaultRequest 允许我们为所有请求设置默认的 HTTP 头、参数等。

kotlin
@TestMethodOrder(OrderAnnotation::class)
class UserControllerTest {
    
    private lateinit var mockMvc: MockMvc
    
    @BeforeEach
    fun setup() {
        mockMvc = MockMvcBuilders.standaloneSetup(UserController())
            .defaultRequest(
                get("/") 
                    .accept(MediaType.APPLICATION_JSON) 
                    .header("X-Client-Version", "1.0.0") 
                    .param("source", "test") 
            )
            .build()
    }
    
    @Test
    @Order(1)
    fun `测试获取用户信息 - 自动应用默认配置`() {
        // 这个请求会自动包含上面设置的默认头和参数
        mockMvc.perform(get("/api/users/1")) 
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").exists())
    }
}

2. 总是期望的响应 (alwaysExpect)

alwaysExpect 设置所有请求都应该满足的响应条件。

kotlin
@BeforeEach
fun setup() {
    mockMvc = MockMvcBuilders.standaloneSetup(UserController())
        .alwaysExpect(status().isOk()) 
        .alwaysExpect(content().contentType("application/json;charset=UTF-8")) 
        .alwaysExpect(header().string("X-Response-Time").exists()) 
        .build()
}

@Test
fun `所有成功的API调用都会自动验证基本响应格式`() {
    // 无需重复写 status().isOk() 和 contentType 的验证
    mockMvc.perform(get("/api/users/1"))
        .andExpect(jsonPath("$.id").value(1)) // 只需关注业务逻辑验证
}

WARNING

使用 alwaysExpect 时要谨慎,确保设置的期望对所有测试都适用。如果某个测试需要验证错误状态码,可能需要单独配置 MockMvc 实例。

3. 自定义配置器 (MockMvcConfigurer)

Spring 提供了 MockMvcConfigurer 接口,允许我们创建可重用的配置组件。

内置配置器:SharedHttpSession

kotlin
@BeforeEach
fun setup() {
    mockMvc = MockMvcBuilders.standaloneSetup(UserController())
        .apply(SharedHttpSessionConfigurer.sharedHttpSession()) 
        .build()
}

@Test
fun `测试需要会话状态的操作`() {
    // 第一个请求:登录
    mockMvc.perform(
        post("/api/login")
            .param("username", "admin")
            .param("password", "123456")
    )
    .andExpect(status().isOk())
    
    // 第二个请求:访问需要登录的接口
    // 会话会自动保持,无需手动处理 Cookie
    mockMvc.perform(get("/api/profile")) 
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.username").value("admin"))
}

自定义配置器

我们也可以创建自己的配置器来封装常用的测试设置:

kotlin
// 自定义配置器
class ApiTestConfigurer : MockMvcConfigurer {
    override fun afterConfigurerAdded(builder: ConfigurableMockMvcBuilder<*>) {
        builder
            .defaultRequest(
                get("/")
                    .accept(MediaType.APPLICATION_JSON)
                    .header("X-API-Version", "v1")
                    .header("X-Client-Type", "test")
            )
            .alwaysExpect(status().isOk())
            .alwaysExpect(content().contentType(MediaType.APPLICATION_JSON))
            .alwaysExpect(header().string("X-Response-Time").exists())
    }
}

// 使用自定义配置器
@TestMethodOrder(OrderAnnotation::class)
class ProductControllerTest {
    
    private lateinit var mockMvc: MockMvc
    
    @BeforeEach
    fun setup() {
        mockMvc = MockMvcBuilders.standaloneSetup(ProductController())
            .apply(ApiTestConfigurer()) 
            .build()
    }
    
    @Test
    @Order(1)
    fun `测试商品列表接口`() {
        mockMvc.perform(get("/api/products"))
            .andExpect(jsonPath("$.data").isArray())
            .andExpect(jsonPath("$.total").isNumber())
    }
}

实际业务场景应用

场景1:API 版本控制测试

kotlin
@TestMethodOrder(OrderAnnotation::class)
class ApiVersionTest {
    
    private lateinit var mockMvcV1: MockMvc
    private lateinit var mockMvcV2: MockMvc
    
    @BeforeEach
    fun setup() {
        // V1 API 配置
        mockMvcV1 = MockMvcBuilders.standaloneSetup(UserControllerV1())
            .defaultRequest(
                get("/")
                    .header("X-API-Version", "v1") 
                    .accept(MediaType.APPLICATION_JSON)
            )
            .alwaysExpect(status().isOk())
            .build()
        
        // V2 API 配置
        mockMvcV2 = MockMvcBuilders.standaloneSetup(UserControllerV2())
            .defaultRequest(
                get("/")
                    .header("X-API-Version", "v2") 
                    .accept(MediaType.APPLICATION_JSON)
            )
            .alwaysExpect(status().isOk())
            .build()
    }
    
    @Test
    @Order(1)
    fun `测试V1和V2接口的响应格式差异`() {
        // V1 接口返回简单格式
        mockMvcV1.perform(get("/api/users/1"))
            .andExpect(jsonPath("$.name").exists())
            .andExpect(jsonPath("$.email").exists())
        
        // V2 接口返回增强格式
        mockMvcV2.perform(get("/api/users/1"))
            .andExpect(jsonPath("$.profile.name").exists())
            .andExpect(jsonPath("$.profile.email").exists())
            .andExpect(jsonPath("$.metadata.version").value("v2"))
    }
}

场景2:多租户系统测试

kotlin
class MultiTenantConfigurer(private val tenantId: String) : MockMvcConfigurer {
    override fun afterConfigurerAdded(builder: ConfigurableMockMvcBuilder<*>) {
        builder.defaultRequest(
            get("/")
                .header("X-Tenant-ID", tenantId) 
                .accept(MediaType.APPLICATION_JSON)
        )
    }
}

@TestMethodOrder(OrderAnnotation::class)
class MultiTenantTest {
    
    private lateinit var tenantAMockMvc: MockMvc
    private lateinit var tenantBMockMvc: MockMvc
    
    @BeforeEach
    fun setup() {
        tenantAMockMvc = MockMvcBuilders.standaloneSetup(OrderController())
            .apply(MultiTenantConfigurer("tenant-a")) 
            .build()
        
        tenantBMockMvc = MockMvcBuilders.standaloneSetup(OrderController())
            .apply(MultiTenantConfigurer("tenant-b")) 
            .build()
    }
    
    @Test
    @Order(1)
    fun `测试不同租户的数据隔离`() {
        // 租户A的订单
        tenantAMockMvc.perform(get("/api/orders"))
            .andExpect(jsonPath("$.data[0].tenantId").value("tenant-a"))
        
        // 租户B的订单
        tenantBMockMvc.perform(get("/api/orders"))
            .andExpect(jsonPath("$.data[0].tenantId").value("tenant-b"))
    }
}

测试流程可视化

最佳实践建议

1. 合理使用 alwaysExpect

注意事项

不要在 alwaysExpect 中设置过于具体的期望,这可能会限制测试的灵活性。

kotlin
// ❌ 不推荐:过于具体
.alwaysExpect(jsonPath("$.code").value(200))
.alwaysExpect(jsonPath("$.message").value("success"))

// ✅ 推荐:通用的格式验证
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType(MediaType.APPLICATION_JSON))

2. 创建可重用的配置器

kotlin
// 为不同类型的测试创建专门的配置器
object TestConfigurers {
    fun apiTest() = ApiTestConfigurer()
    fun adminTest() = AdminTestConfigurer()
    fun mobileTest() = MobileTestConfigurer()
}

// 使用时更加语义化
mockMvc = MockMvcBuilders.standaloneSetup(controller)
    .apply(TestConfigurers.apiTest()) 
    .build()

3. 环境特定配置

kotlin
@TestProfile("integration")
class IntegrationTestConfigurer : MockMvcConfigurer {
    override fun afterConfigurerAdded(builder: ConfigurableMockMvcBuilder<*>) {
        builder
            .defaultRequest(
                get("/")
                    .header("X-Environment", "integration")
                    .header("X-Trace-Enabled", "true")
            )
    }
}

总结

MockMvc Setup Features 通过提供统一的配置机制,让我们能够:

  • 减少重复代码 📝:避免在每个测试中重复设置相同的请求头和响应期望
  • 提高一致性 🎯:确保所有测试都遵循相同的基础规则
  • 增强可维护性 🔧:配置集中管理,修改时只需要改一个地方
  • 支持复杂场景 🚀:通过自定义配置器支持多租户、版本控制等复杂测试场景

TIP

Setup Features 的核心价值在于让测试代码更加专注于业务逻辑的验证,而不是被技术细节所干扰。这正体现了"测试即文档"的理念——好的测试应该清晰地表达业务意图。

通过合理使用这些功能,我们可以构建出既简洁又强大的测试套件,为代码质量保驾护航! ✅