Skip to content

Spring MockMvc 期望定义详解 🧪

什么是期望定义?为什么需要它?

在 Web 应用开发中,我们经常需要验证 HTTP 请求的响应是否符合预期。想象一下,你开发了一个用户管理系统的 API,当客户端请求获取用户信息时,你需要确保:

  • 响应状态码是 200 OK
  • 返回的内容类型是 JSON
  • 响应体包含正确的用户数据
  • 如果用户不存在,返回 404 状态码

NOTE

MockMvc 的期望定义(Defining Expectations)就是用来解决这个问题的核心机制。它让我们能够在测试中精确地验证 HTTP 响应的各个方面,确保我们的 Web 应用按预期工作。

核心概念:andExpect() 与 andExpectAll()

基础期望验证 - andExpect()

andExpect() 是最基础的期望验证方法,它允许我们对 HTTP 响应进行单个断言:

kotlin
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.MockMvc
import org.springframework.http.MediaType

@Test
fun `获取账户信息应该返回200状态码`() {
    mockMvc.get("/accounts/1").andExpect {
        status { isOk() } 
    }
}
java
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*
mockMvc.perform(get("/accounts/1"))
    .andExpect(status().isOk()); 

IMPORTANT

使用 andExpect() 时要注意:一旦某个期望失败,后续的期望将不会被执行。这是"快速失败"的设计哲学。

批量期望验证 - andExpectAll()

当你需要验证多个方面时,andExpectAll() 提供了更好的选择:

kotlin
@Test
fun `获取账户信息应该返回正确的状态码和内容类型`() {
    mockMvc.get("/accounts/1").andExpectAll {
        status { isOk() } 
        content { contentType(APPLICATION_JSON) } 
    }
}
java
mockMvc.perform(get("/accounts/1")).andExpectAll(
    status().isOk(), 
    content().contentType("application/json;charset=UTF-8")); 

TIP

andExpectAll() 的优势在于:即使某个期望失败,它仍会继续执行所有其他期望,并将所有失败信息一起报告。这对于调试非常有用!

期望验证的两大类别

第一类:响应属性验证

这是最常用的验证类型,主要验证 HTTP 响应的基本属性:

kotlin
@RestController
class UserController {
    
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: Long): ResponseEntity<User> {
        val user = userService.findById(id)
        return if (user != null) {
            ResponseEntity.ok(user) 
        } else {
            ResponseEntity.notFound().build() 
        }
    }
}

// 对应的测试
@Test
fun `获取存在的用户应该返回用户信息`() {
    mockMvc.get("/users/1").andExpectAll {
        status { isOk() } 
        content { 
            contentType(APPLICATION_JSON) 
            jsonPath("$.name") { value("张三") } 
            jsonPath("$.email") { value("[email protected]") } 
        }
    }
}

@Test
fun `获取不存在的用户应该返回404`() {
    mockMvc.get("/users/999").andExpect {
        status { isNotFound() } 
    }
}

第二类:Spring MVC 内部状态验证

这类验证让我们能够检查 Spring MVC 的内部处理过程:

kotlin
@Controller
class PersonController {
    
    @PostMapping("/persons")
    fun createPerson(@Valid @ModelAttribute person: Person, 
                    bindingResult: BindingResult): String {
        if (bindingResult.hasErrors()) {
            return "person/form"
        }
        personService.save(person)
        return "redirect:/persons"
    }
}

// 验证表单验证失败的情况
@Test
fun `提交无效的用户数据应该返回表单页面并包含错误信息`() {
    mockMvc.post("/persons") {
        param("name", "") // 空名称,触发验证错误
        param("email", "invalid-email") // 无效邮箱格式
    }.andExpectAll {
        status { isOk() }
        model { 
            attributeHasErrors("person") 
        }
        view { name("person/form") } 
    }
}

调试利器:打印和日志

使用 print() 调试响应

在开发过程中,我们经常需要查看完整的响应内容来调试问题:

kotlin
@Test
fun `调试用户创建接口的完整响应`() {
    mockMvc.post("/persons") {
        param("name", "李四")
        param("email", "[email protected]")
    }.andDo {
        print() // [!code highlight] // 打印完整的响应信息到控制台
    }.andExpectAll {
        status { isOk() }
        model { 
            attributeHasNoErrors("person")
        }
    }
}

TIP

print() 方法会输出详细的请求和响应信息,包括:

  • HTTP 方法和 URL
  • 请求头和参数
  • 响应状态码和头部
  • 响应体内容
  • Spring MVC 处理信息(如使用的控制器方法、视图名称等)

自定义输出目标

kotlin
// 输出到错误流
.andDo { print(System.err) }

// 输出到自定义 Writer
val stringWriter = StringWriter()
.andDo { print(PrintWriter(stringWriter)) }

// 使用日志记录(DEBUG 级别)
.andDo { log() } 

高级用法:获取响应结果进行自定义验证

有时候,标准的期望验证无法满足复杂的业务逻辑验证需求。这时可以使用 andReturn() 获取完整的响应结果:

kotlin
@Test
fun `验证复杂的业务逻辑响应`() {
    val mvcResult = mockMvc.post("/orders") {
        contentType(APPLICATION_JSON)
        content("""
            {
                "productId": 1,
                "quantity": 5,
                "customerId": 100
            }
        """.trimIndent())
    }.andExpect {
        status { isCreated() }
    }.andReturn() 
    
    // 获取响应内容进行自定义验证
    val responseContent = mvcResult.response.contentAsString
    val orderResponse = objectMapper.readValue(responseContent, OrderResponse::class.java)
    
    // 自定义业务逻辑验证
    assertThat(orderResponse.orderId).isNotNull() 
    assertThat(orderResponse.totalAmount).isEqualTo(BigDecimal("299.95")) 
    assertThat(orderResponse.estimatedDelivery).isAfter(LocalDate.now()) 
}

全局期望配置:避免重复代码

当多个测试都需要相同的基础验证时,可以在构建 MockMvc 时设置全局期望:

java
@BeforeEach
void setUp() {
    mockMvc = MockMvcBuilders
        .standaloneSetup(new UserController())
        .alwaysExpect(status().isOk()) 
        .alwaysExpect(content().contentType("application/json;charset=UTF-8")) 
        .build();
}
kotlin
// 注意:由于 Kotlin 的限制(KT-22208),
// 目前无法在 Kotlin 中使用全局期望配置
// 需要在每个测试中单独设置期望

WARNING

全局期望会应用到所有使用该 MockMvc 实例的测试中,且无法被覆盖。如果需要不同的期望,必须创建单独的 MockMvc 实例。

实际应用场景示例

场景1:RESTful API 测试

kotlin
@RestController
class ProductController(private val productService: ProductService) {
    
    @GetMapping("/api/products/{id}")
    fun getProduct(@PathVariable id: Long): ResponseEntity<Product> {
        return productService.findById(id)
            ?.let { ResponseEntity.ok(it) }
            ?: ResponseEntity.notFound().build()
    }
    
    @PostMapping("/api/products")
    fun createProduct(@Valid @RequestBody product: Product): ResponseEntity<Product> {
        val savedProduct = productService.save(product)
        return ResponseEntity.status(HttpStatus.CREATED).body(savedProduct)
    }
}

// 完整的测试示例
@WebMvcTest(ProductController::class)
class ProductControllerTest {
    
    @Autowired
    private lateinit var mockMvc: MockMvc
    
    @MockBean
    private lateinit var productService: ProductService
    
    @Test
    fun `获取存在的商品应该返回商品信息`() {
        // 准备测试数据
        val product = Product(1L, "iPhone 15", BigDecimal("7999.00"))
        given(productService.findById(1L)).willReturn(product)
        
        // 执行测试并验证
        mockMvc.get("/api/products/1").andExpectAll {
            status { isOk() } 
            content { 
                contentType(APPLICATION_JSON) 
                jsonPath("$.id") { value(1) } 
                jsonPath("$.name") { value("iPhone 15") } 
                jsonPath("$.price") { value(7999.00) } 
            }
        }
    }
    
    @Test
    fun `创建商品应该返回201状态码和商品信息`() {
        val newProduct = Product(null, "MacBook Pro", BigDecimal("12999.00"))
        val savedProduct = Product(2L, "MacBook Pro", BigDecimal("12999.00"))
        given(productService.save(any())).willReturn(savedProduct)
        
        mockMvc.post("/api/products") {
            contentType(APPLICATION_JSON)
            content("""
                {
                    "name": "MacBook Pro",
                    "price": 12999.00
                }
            """.trimIndent())
        }.andExpectAll {
            status { isCreated() } 
            content {
                contentType(APPLICATION_JSON)
                jsonPath("$.id") { value(2) } 
                jsonPath("$.name") { value("MacBook Pro") }
                jsonPath("$.price") { value(12999.00) }
            }
        }
    }
}

场景2:HATEOAS 链接验证

当你的 API 使用 Spring HATEOAS 提供超媒体链接时:

kotlin
@Test
fun `获取用户列表应该包含正确的HATEOAS链接`() {
    mockMvc.get("/people") {
        accept(APPLICATION_JSON)
    }.andExpectAll {
        status { isOk() }
        content { contentType(APPLICATION_JSON) }
        // 验证自引用链接
        jsonPath("$.links[?(@.rel == 'self')].href") { 
            value("http://localhost:8080/people")
        }
        // 验证下一页链接
        jsonPath("$.links[?(@.rel == 'next')].href") { 
            value("http://localhost:8080/people?page=1")
        }
    }
}

测试流程图

最佳实践建议

1. 选择合适的验证方法

使用建议

  • 单一验证:使用 andExpect(),适合简单的状态码检查
  • 多重验证:使用 andExpectAll(),能够获得更完整的错误信息
  • 复杂验证:使用 andReturn() 获取结果进行自定义验证

2. 合理使用调试工具

注意事项

  • 在开发阶段使用 print() 调试响应内容
  • 生产测试中移除 print() 调用,避免日志污染
  • 使用 log() 替代 print() 进行生产环境调试

3. 期望验证的层次性

kotlin
// 推荐:从基础到具体的验证顺序
mockMvc.get("/api/users/1").andExpectAll {
    // 1. 首先验证基础响应属性
    status { isOk() }
    content { contentType(APPLICATION_JSON) }
    
    // 2. 然后验证响应体结构
    jsonPath("$") { exists() }
    jsonPath("$.id") { exists() }
    
    // 3. 最后验证具体的业务数据
    jsonPath("$.id") { value(1) }
    jsonPath("$.name") { value("张三") }
    jsonPath("$.email") { value("[email protected]") }
}

总结

MockMvc 的期望定义是 Spring Boot 测试框架中的核心功能,它提供了:

灵活的验证方式andExpect()andExpectAll() 满足不同场景需求

全面的验证能力:从 HTTP 响应属性到 Spring MVC 内部状态的全方位验证

强大的调试支持print()log() 方法帮助快速定位问题

可扩展性:通过 andReturn() 支持复杂的自定义验证逻辑

IMPORTANT

掌握期望定义的使用,能够让你编写出更加可靠和全面的 Web 应用测试,确保你的 API 在各种场景下都能正确工作。记住:好的测试不仅要验证成功的情况,更要覆盖各种异常和边界情况!