Appearance
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 在各种场景下都能正确工作。记住:好的测试不仅要验证成功的情况,更要覆盖各种异常和边界情况!