Skip to content

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 的其他功能,如响应验证和断言,将让你的测试技能更加完善! 🚀