Appearance
Spring MockMvcTester 执行请求详解 🚀
概述
在 Spring Boot 测试中,MockMvcTester
是一个强大的工具,它结合了 AssertJ 的流畅断言 API,让我们能够优雅地执行 HTTP 请求并验证响应。相比传统的 MockMvc,它提供了更加直观和易用的测试体验。
NOTE
MockMvcTester 是 Spring Framework 新引入的测试工具,它将 MockMvc 的功能与 AssertJ 的断言能力完美结合,为开发者提供了更加流畅的测试体验。
核心价值与解决的问题 💡
传统测试方式的痛点
在没有 MockMvcTester 之前,我们进行 Web 层测试时面临以下挑战:
- 语法冗长:需要导入大量静态方法
- 断言复杂:Hamcrest 匹配器学习成本高
- 可读性差:测试代码难以理解和维护
MockMvcTester 的解决方案
kotlin
// 传统 MockMvc + Hamcrest 方式
mockMvc.perform(post("/hotels/{id}", 42)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.name").value("Grand Hotel"))
kotlin
// 新的 MockMvcTester + AssertJ 方式
assertThat(mockMvc.post().uri("/hotels/{id}", 42)
.accept(MediaType.APPLICATION_JSON))
.hasStatusOk()
.hasContentType(MediaType.APPLICATION_JSON)
.bodyJson().extractingPath("$.name").isEqualTo("Grand Hotel")
基础请求执行 📝
简单请求示例
让我们从一个简单的酒店管理系统开始:
kotlin
@RestController
class HotelController {
@PostMapping("/hotels/{id}")
fun updateHotel(
@PathVariable id: Long,
@RequestBody hotel: Hotel
): ResponseEntity<Hotel> {
// 业务逻辑处理
val updatedHotel = hotelService.update(id, hotel)
return ResponseEntity.ok(updatedHotel)
}
}
使用 MockMvcTester 测试这个接口:
kotlin
@Test
fun `should update hotel successfully`() {
// 执行 POST 请求并验证响应
assertThat(mockMvc.post().uri("/hotels/{id}", 42)
.accept(MediaType.APPLICATION_JSON))
.hasStatusOk()
.hasContentType(MediaType.APPLICATION_JSON)
}
TIP
注意这里不需要导入任何静态方法,所有的操作都通过 mockMvc
实例的流畅 API 完成。
分离式断言
当需要对响应进行多个方面的验证时,可以使用 .exchange()
方法获取结果对象:
kotlin
@Test
fun `should validate multiple aspects of response`() {
// 执行请求并获取结果
val result = mockMvc.post().uri("/hotels/{id}", 42)
.accept(MediaType.APPLICATION_JSON)
.exchange()
// 分别验证不同方面
assertThat(result)
.hasStatusOk()
.hasContentType(MediaType.APPLICATION_JSON)
assertThat(result)
.bodyJson()
.extractingPath("$.id").isEqualTo(42)
assertThat(result)
.bodyJson()
.extractingPath("$.name").isNotNull()
}
参数传递方式 🔧
URI 模板参数
MockMvcTester 支持多种参数传递方式,让我们看看查询参数的处理:
kotlin
@GetMapping("/hotels")
fun searchHotels(
@RequestParam location: String,
@RequestParam(required = false) rating: Int?
): List<Hotel> {
return hotelService.search(location, rating)
}
方式一:URI 模板风格
kotlin
@Test
fun `should search hotels with query parameters using URI template`() {
assertThat(mockMvc.get().uri("/hotels?location={location}&rating={rating}",
"Beijing", 5))
.hasStatusOk()
}
方式二:param() 方法
kotlin
@Test
fun `should search hotels with query parameters using param method`() {
assertThat(mockMvc.get().uri("/hotels")
.param("location", "Beijing")
.param("rating", "5"))
.hasStatusOk()
}
IMPORTANT
两种方式的重要区别:
- URI 模板中的参数会被自动 URL 编码
param()
方法的参数需要预先编码
实际业务场景对比
kotlin
@Test
fun `should handle special characters in location`() {
val location = "São Paulo" // 包含特殊字符
// URI 模板方式 - 自动编码 ✅
assertThat(mockMvc.get().uri("/hotels?location={location}", location))
.hasStatusOk()
// param 方式 - 需要手动处理编码
assertThat(mockMvc.get().uri("/hotels")
.param("location", URLEncoder.encode(location, "UTF-8")))
.hasStatusOk()
}
kotlin
@Test
fun `should search with simple parameters`() {
// 对于简单参数,两种方式都很方便
assertThat(mockMvc.get().uri("/hotels")
.param("location", "Beijing")
.param("rating", "5"))
.hasStatusOk()
}
异步请求处理 ⏰
现代 Web 应用经常使用异步处理来提高性能,MockMvcTester 对此提供了完善的支持。
异步控制器示例
kotlin
@RestController
class ComputeController {
@GetMapping("/compute")
fun performHeavyComputation(): DeferredResult<String> {
val deferredResult = DeferredResult<String>(10000L) // 10秒超时
// 异步处理
CompletableFuture.supplyAsync {
Thread.sleep(2000) // 模拟耗时操作
"Computation completed"
}.whenComplete { result, throwable ->
if (throwable != null) {
deferredResult.setErrorResult(throwable)
} else {
deferredResult.setResult(result)
}
}
return deferredResult
}
}
异步请求测试
kotlin
@Test
fun `should wait for async request completion`() {
// 默认等待 10 秒
assertThat(mockMvc.get().uri("/compute").exchange())
.hasStatusOk()
.bodyText().isEqualTo("Computation completed")
}
@Test
fun `should wait for async request with custom timeout`() {
// 自定义超时时间为 5 秒
assertThat(mockMvc.get().uri("/compute")
.exchange(Duration.ofSeconds(5)))
.hasStatusOk()
}
手动管理异步生命周期
如果需要更精细的控制,可以使用 asyncExchange()
:
kotlin
@Test
fun `should manage async lifecycle manually`() {
val result = mockMvc.get().uri("/compute")
.asyncExchange()
// 可以在这里执行其他操作
// ...
// 手动等待完成
assertThat(result.await())
.hasStatusOk()
}
WARNING
使用 asyncExchange()
时,需要手动调用 await()
方法等待异步操作完成,否则可能获取到不完整的结果。
文件上传处理 📁
文件上传是 Web 应用的常见功能,MockMvcTester 提供了简洁的多部分请求支持。
文件上传控制器
kotlin
@RestController
class FileUploadController {
@PostMapping("/upload")
fun uploadFiles(
@RequestParam("files") files: Array<MultipartFile>,
@RequestParam("description") description: String
): ResponseEntity<UploadResult> {
val uploadedFiles = files.map { file ->
FileInfo(
name = file.originalFilename ?: "unknown",
size = file.size,
contentType = file.contentType ?: "application/octet-stream"
)
}
return ResponseEntity.ok(
UploadResult(
description = description,
files = uploadedFiles,
totalSize = uploadedFiles.sumOf { it.size }
)
)
}
}
data class FileInfo(
val name: String,
val size: Long,
val contentType: String
)
data class UploadResult(
val description: String,
val files: List<FileInfo>,
val totalSize: Long
)
多文件上传测试
kotlin
@Test
fun `should upload multiple files successfully`() {
val file1Content = "Hello World".toByteArray(StandardCharsets.UTF_8)
val file2Content = "Spring Boot".toByteArray(StandardCharsets.UTF_8)
assertThat(mockMvc.post().uri("/upload")
.multipart()
.file("files", "hello.txt", file1Content)
.file("files", "spring.txt", file2Content)
.param("description", "Test upload"))
.hasStatusOk()
.bodyJson()
.extractingPath("$.totalSize")
.isEqualTo(file1Content.size + file2Content.size)
}
复杂文件上传场景
kotlin
@Test
fun `should upload files with metadata`() {
val jsonMetadata = """
{
"category": "documents",
"tags": ["important", "confidential"]
}
""".trimIndent()
assertThat(mockMvc.post().uri("/upload")
.multipart()
.file("files", "document.pdf", "PDF content".toByteArray())
.file("metadata", "metadata.json",
jsonMetadata.toByteArray(StandardCharsets.UTF_8))
.param("description", "Document with metadata"))
.hasStatusOk()
}
NOTE
MockMvcTester 的 multipart 支持内部使用 MockMultipartHttpServletRequest
,这意味着不会进行真正的多部分解析,而是直接设置请求数据。
上下文路径和 Servlet 路径 🛣️
在实际部署环境中,应用可能运行在特定的上下文路径下,MockMvcTester 提供了完整的支持。
场景说明
单次请求指定路径
kotlin
@Test
fun `should handle context and servlet paths`() {
assertThat(mockMvc.get()
.uri("/app/api/hotels/{id}", 42)
.contextPath("/app")
.servletPath("/api"))
.hasStatusOk()
}
全局默认路径配置
当所有请求都需要相同的路径配置时,可以设置默认值:
kotlin
class HotelControllerTest {
private val mockMvc = MockMvcTester.of(listOf(HotelController())) { builder ->
builder.defaultRequest(
MockMvcRequestBuilders.get("/")
.contextPath("/app")
.servletPath("/api")
.accept(MediaType.APPLICATION_JSON)
).build()
}
@Test
fun `should use default paths`() {
// 不需要重复指定 contextPath 和 servletPath
assertThat(mockMvc.get().uri("/hotels/{id}", 42))
.hasStatusOk()
}
@Test
fun `should override default paths when needed`() {
// 可以覆盖默认设置
assertThat(mockMvc.get().uri("/different/path/hotels/{id}", 42)
.contextPath("/different"))
.hasStatusOk()
}
}
最佳实践与建议 ✨
1. 选择合适的断言方式
建议
- 单一验证:直接使用
assertThat(mockMvc.get()...)
- 多重验证:使用
.exchange()
获取结果对象 - 异步操作:根据需要选择
exchange()
或asyncExchange()
2. 参数传递策略
kotlin
// ✅ 推荐:简单参数使用 param()
assertThat(mockMvc.get().uri("/search")
.param("keyword", "hotel")
.param("page", "1"))
// ✅ 推荐:复杂参数使用 URI 模板
assertThat(mockMvc.get().uri("/search?q={query}&location={loc}",
"luxury hotel", "New York"))
3. 测试组织结构
kotlin
@WebMvcTest(HotelController::class)
class HotelControllerTest {
@Autowired
private lateinit var mockMvc: MockMvcTester
@MockBean
private lateinit var hotelService: HotelService
@Nested
inner class GetHotelTests {
// GET 相关测试
}
@Nested
inner class CreateHotelTests {
// POST 相关测试
}
@Nested
inner class FileUploadTests {
// 文件上传测试
}
}
总结 🎯
MockMvcTester 通过以下方式革新了 Spring Boot 的 Web 层测试:
- 流畅的 API:无需导入静态方法,代码更加清晰
- 强大的断言:结合 AssertJ 提供丰富的验证能力
- 异步支持:完善的异步请求处理机制
- 灵活配置:支持各种请求参数和路径配置
IMPORTANT
MockMvcTester 不仅仅是一个测试工具,它代表了 Spring 团队对开发者体验的持续改进。通过提供更直观、更强大的测试 API,它让我们能够编写更好的测试,从而构建更可靠的应用程序。
通过掌握这些技巧,你将能够编写出既简洁又全面的 Web 层测试,确保你的 Spring Boot 应用在各种场景下都能正常工作! 🚀