Appearance
Spring MockMvc 请求执行指南 🚀
概述
MockMvc 是 Spring Test 框架中的核心组件,专门用于测试 Spring MVC 控制器。它允许我们在不启动完整 Web 服务器的情况下,模拟 HTTP 请求并验证响应结果。
NOTE
本文档重点介绍如何直接使用 MockMvc 执行请求。如果你使用的是 WebTestClient
,请参考相应的测试编写文档。
为什么需要 MockMvc? 🤔
在传统的 Web 应用测试中,我们面临以下痛点:
- 启动成本高:需要启动完整的 Web 服务器才能测试控制器
- 测试速度慢:网络 I/O 和服务器启动时间影响测试效率
- 环境依赖:需要配置复杂的测试环境
- 隔离性差:难以进行单元级别的精确测试
MockMvc 的设计哲学是**"轻量级模拟,精确验证"**,它通过模拟 Servlet 容器环境,让我们能够:
✅ 快速测试控制器逻辑
✅ 验证请求映射和参数绑定
✅ 检查响应状态和内容
✅ 保持测试的独立性和可重复性
基本请求执行
HTTP 方法请求
MockMvc 支持所有标准的 HTTP 方法,让我们看看如何执行不同类型的请求:
kotlin
import org.springframework.test.web.servlet.post
import org.springframework.http.MediaType
// POST 请求示例
mockMvc.post("/hotels/{id}", 42) {
accept = MediaType.APPLICATION_JSON
}
// GET 请求示例
mockMvc.get("/hotels") {
accept = MediaType.APPLICATION_JSON
}
java
// 需要静态导入 MockMvcRequestBuilders.*
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
// POST 请求示例
mockMvc.perform(post("/hotels/{id}", 42)
.accept(MediaType.APPLICATION_JSON));
// GET 请求示例
mockMvc.perform(get("/hotels")
.accept(MediaType.APPLICATION_JSON));
TIP
Kotlin DSL 提供了更简洁的语法,推荐在 Kotlin 项目中使用。路径变量 {id}
会自动替换为提供的参数值 42
。
文件上传请求
处理文件上传是 Web 应用的常见需求,MockMvc 提供了便捷的文件上传测试支持:
kotlin
import org.springframework.test.web.servlet.multipart
// 单文件上传测试
mockMvc.multipart("/doc") {
file("a1", "ABC".toByteArray(charset("UTF8")))
}
// 多文件上传测试
mockMvc.multipart("/documents") {
file("file1", "内容1".toByteArray())
file("file2", "内容2".toByteArray())
param("description", "批量上传测试")
}
java
// 单文件上传测试
mockMvc.perform(multipart("/doc")
.file("a1", "ABC".getBytes("UTF-8")));
// 多文件上传测试
mockMvc.perform(multipart("/documents")
.file("file1", "内容1".getBytes("UTF-8"))
.file("file2", "内容2".getBytes("UTF-8"))
.param("description", "批量上传测试"));
IMPORTANT
MockMvc 使用 MockMultipartHttpServletRequest
进行文件上传模拟,这意味着不会进行真实的 multipart 请求解析,而是直接设置文件内容。
请求参数处理
URI 模板参数
URI 模板是 RESTful API 设计的重要组成部分,MockMvc 完美支持这种参数传递方式:
kotlin
// 单个路径参数
mockMvc.get("/hotels/{id}", 123)
// 多个路径参数
mockMvc.get("/hotels/{id}/rooms/{roomId}", 123, 456)
// 查询参数模板
mockMvc.get("/hotels?thing={thing}", "somewhere")
java
// 单个路径参数
mockMvc.perform(get("/hotels/{id}", 123));
// 多个路径参数
mockMvc.perform(get("/hotels/{id}/rooms/{roomId}", 123, 456));
// 查询参数模板
mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));
Servlet 请求参数
除了 URI 模板,我们还可以通过 param()
方法添加请求参数:
kotlin
import org.springframework.test.web.servlet.get
mockMvc.get("/hotels") {
param("thing", "somewhere")
param("category", "luxury")
param("minPrice", "100")
}
java
mockMvc.perform(get("/hotels")
.param("thing", "somewhere")
.param("category", "luxury")
.param("minPrice", "100"));
WARNING
注意参数编码差异:URI 模板中的查询参数会被自动解码,而通过 param()
方法提供的参数应该是已解码的状态。
实际业务场景示例
让我们通过一个完整的酒店预订系统来演示 MockMvc 的实际应用:
酒店控制器完整示例
kotlin
@RestController
@RequestMapping("/api/hotels")
class HotelController(
private val hotelService: HotelService
) {
@GetMapping("/{id}")
fun getHotel(@PathVariable id: Long): ResponseEntity<Hotel> {
val hotel = hotelService.findById(id)
return ResponseEntity.ok(hotel)
}
@PostMapping
fun createHotel(@RequestBody hotel: Hotel): ResponseEntity<Hotel> {
val savedHotel = hotelService.save(hotel)
return ResponseEntity.status(HttpStatus.CREATED).body(savedHotel)
}
@GetMapping
fun searchHotels(
@RequestParam(required = false) city: String?,
@RequestParam(required = false) minPrice: BigDecimal?,
@RequestParam(required = false) maxPrice: BigDecimal?
): ResponseEntity<List<Hotel>> {
val hotels = hotelService.search(city, minPrice, maxPrice)
return ResponseEntity.ok(hotels)
}
@PostMapping("/{id}/images")
fun uploadHotelImage(
@PathVariable id: Long,
@RequestParam("image") file: MultipartFile
): ResponseEntity<String> {
val imageUrl = hotelService.uploadImage(id, file)
return ResponseEntity.ok(imageUrl)
}
}
对应的测试代码:
kotlin
@WebMvcTest(HotelController::class)
class HotelControllerTest {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
private lateinit var hotelService: HotelService
@Test
fun `should get hotel by id`() {
// Given
val hotel = Hotel(id = 1L, name = "豪华酒店", city = "北京")
given(hotelService.findById(1L)).willReturn(hotel)
// When & Then
mockMvc.get("/api/hotels/{id}", 1L) {
accept = MediaType.APPLICATION_JSON
}.andExpect {
status { isOk() }
jsonPath("$.name") { value("豪华酒店") }
jsonPath("$.city") { value("北京") }
}
}
@Test
fun `should create new hotel`() {
// Given
val newHotel = Hotel(name = "新酒店", city = "上海")
val savedHotel = Hotel(id = 2L, name = "新酒店", city = "上海")
given(hotelService.save(any())).willReturn(savedHotel)
// When & Then
mockMvc.post("/api/hotels") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(newHotel)
}.andExpect {
status { isCreated() }
jsonPath("$.id") { value(2L) }
}
}
@Test
fun `should search hotels with parameters`() {
// Given
val hotels = listOf(
Hotel(id = 1L, name = "北京酒店", city = "北京"),
Hotel(id = 2L, name = "北京豪华酒店", city = "北京")
)
given(hotelService.search(eq("北京"), any(), any())).willReturn(hotels)
// When & Then
mockMvc.get("/api/hotels") {
param("city", "北京")
param("minPrice", "100")
param("maxPrice", "500")
}.andExpect {
status { isOk() }
jsonPath("$.length()") { value(2) }
}
}
@Test
fun `should upload hotel image`() {
// Given
val imageUrl = "https://example.com/images/hotel1.jpg"
given(hotelService.uploadImage(eq(1L), any())).willReturn(imageUrl)
// When & Then
mockMvc.multipart("/api/hotels/{id}/images", 1L) {
file("image", "fake-image-content".toByteArray())
}.andExpect {
status { isOk() }
content { string(imageUrl) }
}
}
}
高级配置:上下文路径和 Servlet 路径
在复杂的企业应用中,我们可能需要处理上下文路径和 Servlet 路径的配置:
单次请求配置
kotlin
mockMvc.get("/app/main/hotels/{id}", 123) {
contextPath = "/app"
servletPath = "/main"
}
java
mockMvc.perform(get("/app/main/hotels/{id}", 123)
.contextPath("/app")
.servletPath("/main"));
全局默认配置
如果每个请求都需要相同的路径配置,可以设置默认请求属性:
java
@TestConfiguration
class MockMvcConfig {
@Bean
@Primary
MockMvc mockMvc(WebApplicationContext context) {
return MockMvcBuilders
.webAppContextSetup(context)
.defaultRequest(get("/")
.contextPath("/app")
.servletPath("/main")
.accept(MediaType.APPLICATION_JSON))
.build();
}
}
NOTE
Kotlin DSL 目前不支持默认请求配置,这是由于 Kotlin 的一个已知问题 (KT-22208)。
最佳实践建议 ⭐
1. 请求 URI 设计
推荐做法
kotlin
// ✅ 推荐:使用相对路径
mockMvc.get("/api/hotels/{id}", 123)
// ❌ 避免:包含完整的上下文路径(除非必要)
mockMvc.get("/myapp/api/hotels/{id}", 123)
2. 参数传递方式选择
选择指南
- 路径参数:使用 URI 模板
{id}
- 查询参数:简单场景用 URI 模板,复杂场景用
param()
方法 - 表单参数:使用
param()
方法
3. 测试数据管理
kotlin
class HotelTestData {
companion object {
fun createSampleHotel(id: Long = 1L) = Hotel(
id = id,
name = "测试酒店",
city = "测试城市",
price = BigDecimal("299.00")
)
fun createHotelJson(hotel: Hotel): String {
return """
{
"id": ${hotel.id},
"name": "${hotel.name}",
"city": "${hotel.city}",
"price": ${hotel.price}
}
""".trimIndent()
}
}
}
总结 🎉
MockMvc 作为 Spring Test 框架的核心组件,为我们提供了强大而灵活的 Web 层测试能力。通过本文的学习,你应该掌握了:
- 基本请求执行:GET、POST、PUT、DELETE 等 HTTP 方法的测试
- 文件上传测试:multipart 请求的模拟和验证
- 参数处理:路径参数、查询参数、表单参数的不同处理方式
- 高级配置:上下文路径和默认请求属性的设置
- 实际应用:完整的业务场景测试示例
IMPORTANT
记住 MockMvc 的核心价值:快速、轻量、精确。它让我们能够在不启动完整服务器的情况下,全面测试 Web 层的功能,大大提高了测试效率和开发体验。
继续探索 MockMvc 的其他功能,如响应验证和断言,将让你的测试技能更加完善! 🚀