Skip to content

Spring MockMvc 异步请求测试:让测试跟上现代应用的节拍 🚀

引言:为什么需要异步请求测试?

在现代 Web 应用中,异步处理已经成为提升性能和用户体验的关键技术。想象一下这样的场景:

  • 📊 数据分析报告:用户请求生成复杂报告,需要几秒钟处理时间
  • 📧 邮件发送:批量发送邮件通知,不应阻塞用户界面
  • 🔄 第三方API调用:调用外部服务获取数据,响应时间不可控

如果这些操作都采用同步方式,服务器线程会被长时间占用,导致系统吞吐量急剧下降。这就是为什么 Spring MVC 引入了异步请求处理机制。

IMPORTANT

异步请求处理的核心思想:释放 Servlet 容器线程,让应用在后台异步计算响应,然后通过异步分发完成最终处理

异步请求的工作原理

让我们通过时序图来理解异步请求的完整流程:

MockMvc 异步测试的挑战

在测试环境中,我们面临一个独特的挑战:没有真正运行的 Servlet 容器。这意味着:

测试环境的限制

  • 无法自动触发异步分发
  • 需要手动模拟容器的异步处理流程
  • 必须分步验证异步处理的各个阶段

异步测试的核心步骤

MockMvc 异步测试遵循一个清晰的三阶段模式:

1️⃣ 验证异步启动阶段

2️⃣ 等待并验证异步结果

3️⃣ 手动触发异步分发并验证最终响应

让我们通过实际代码来看看这个过程:

完整的异步测试示例

控制器代码

首先,让我们创建一个支持异步处理的控制器:

kotlin
@RestController
@RequestMapping("/api/async")
class AsyncController {

    @GetMapping("/deferred")
    fun getDeferredResult(): DeferredResult<String> {
        val deferredResult = DeferredResult<String>(5000L) 
        
        // 模拟异步处理
        CompletableFuture.runAsync {
            Thread.sleep(1000) // 模拟耗时操作
            deferredResult.setResult("异步处理完成") 
        }
        
        return deferredResult
    }

    @GetMapping("/callable")
    fun getCallableResult(): Callable<String> {
        return Callable { 
            Thread.sleep(1000) // 模拟耗时操作
            "Callable处理完成"
        }
    }

    @GetMapping("/mono")
    fun getMonoResult(): Mono<String> {
        return Mono.delay(Duration.ofSeconds(1)) 
            .map { "Reactor Mono处理完成" }
    }
}
kotlin
@RestController
@RequestMapping("/api/sync")
class SyncController {

    @GetMapping("/blocking")
    fun getBlockingResult(): String {
        Thread.sleep(1000) // 阻塞Servlet线程
        return "同步处理完成"
    }
}

异步测试代码

现在让我们编写完整的异步测试:

kotlin
@SpringBootTest
@AutoConfigureTestDatabase
@TestMethodOrder(OrderAnnotation::class)
class AsyncControllerTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    @Order(1)
    fun `测试DeferredResult异步处理`() {
        // 第一阶段:发起请求并验证异步启动
        val mvcResult = mockMvc.get("/api/async/deferred")
            .andExpect {
                status { isOk() } 
                request { asyncStarted() } 
                request { asyncResult<Nothing>("异步处理完成") } 
            }
            .andReturn()

        // 第二阶段:手动触发异步分发并验证最终响应
        mockMvc.perform(asyncDispatch(mvcResult)) 
            .andExpect {
                status { isOk() }
                content { string("异步处理完成") }
            }
    }

    @Test
    @Order(2)
    fun `测试Callable异步处理`() {
        val mvcResult = mockMvc.get("/api/async/callable")
            .andExpect {
                status { isOk() }
                request { asyncStarted() }
                request { asyncResult<Nothing>("Callable处理完成") }
            }
            .andReturn()

        mockMvc.perform(asyncDispatch(mvcResult))
            .andExpect {
                status { isOk() }
                content { string("Callable处理完成") }
            }
    }

    @Test
    @Order(3)
    fun `测试Reactor Mono异步处理`() {
        val mvcResult = mockMvc.get("/api/async/mono")
            .andExpect {
                status { isOk() }
                request { asyncStarted() }
                request { asyncResult<Nothing>("Reactor Mono处理完成") }
            }
            .andReturn()

        mockMvc.perform(asyncDispatch(mvcResult))
            .andExpect {
                status { isOk() }
                content { string("Reactor Mono处理完成") }
            }
    }
}

关键API详解

🔍 核心验证方法

方法作用使用场景
request().asyncStarted()验证异步处理已启动确认请求进入异步模式
request().asyncResult(expectedValue)等待并验证异步结果检查异步计算的返回值
asyncDispatch(mvcResult)手动触发异步分发模拟容器的异步分发过程

💡 重要注意事项

异步测试的关键理解

  • 第一次请求:验证异步启动和异步结果
  • 第二次请求:通过 asyncDispatch() 模拟异步分发,验证最终响应
  • 这两个步骤缺一不可,模拟了真实环境中的完整异步处理流程

异步vs同步:性能对比

让我们通过一个实际的压力测试来看看异步处理的优势:

性能测试代码示例
kotlin
@Test
fun `异步vs同步性能对比测试`() {
    val concurrentRequests = 100
    val latch = CountDownLatch(concurrentRequests)
    
    // 测试异步接口
    val asyncStartTime = System.currentTimeMillis()
    repeat(concurrentRequests) {
        CompletableFuture.runAsync {
            try {
                val mvcResult = mockMvc.get("/api/async/deferred")
                    .andExpect { request { asyncStarted() } }
                    .andReturn()
                
                mockMvc.perform(asyncDispatch(mvcResult))
                    .andExpect { status { isOk() } }
            } finally {
                latch.countDown()
            }
        }
    }
    
    latch.await()
    val asyncEndTime = System.currentTimeMillis()
    
    println("异步处理 $concurrentRequests 个请求耗时: ${asyncEndTime - asyncStartTime}ms")
    
    // 对比同步接口的性能...
}

实际业务场景应用

📊 报表生成服务

kotlin
@RestController
class ReportController {

    @GetMapping("/reports/{id}/generate")
    fun generateReport(@PathVariable id: Long): DeferredResult<ReportDto> {
        val deferredResult = DeferredResult<ReportDto>(30000L) // 30秒超时
        
        // 异步生成报表
        reportService.generateReportAsync(id) { report ->
            if (report != null) {
                deferredResult.setResult(report) 
            } else {
                deferredResult.setErrorResult( 
                    ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                        .body("报表生成失败")
                )
            }
        }
        
        return deferredResult
    }
}

🔄 批量数据处理

kotlin
@RestController
class BatchController {

    @PostMapping("/batch/process")
    fun processBatch(@RequestBody items: List<DataItem>): Callable<BatchResult> {
        return Callable { 
            val results = items.parallelStream()
                .map { item -> processItem(item) }
                .collect(Collectors.toList())
            
            BatchResult(
                total = items.size,
                processed = results.size,
                results = results
            )
        }
    }
}

错误处理与超时管理

⚠️ 异步错误处理

kotlin
@Test
fun `测试异步处理超时`() {
    val mvcResult = mockMvc.get("/api/async/timeout")
        .andExpect {
            status { isOk() }
            request { asyncStarted() }
        }
        .andReturn()

    // 验证超时错误
    mockMvc.perform(asyncDispatch(mvcResult))
        .andExpect {
            status { isInternalServerError() } 
            content { string(containsString("请求超时")) }
        }
}

🛡️ 异常处理测试

kotlin
@Test
fun `测试异步处理异常`() {
    val mvcResult = mockMvc.get("/api/async/error")
        .andExpect {
            status { isOk() }
            request { asyncStarted() }
            // 验证异步结果是异常
            request { 
                asyncResult<Exception> { result ->
                    assertThat(result).isInstanceOf(RuntimeException::class.java)
                }
            }
        }
        .andReturn()

    mockMvc.perform(asyncDispatch(mvcResult))
        .andExpect {
            status { isInternalServerError() } 
        }
}

最佳实践总结

✅ 推荐做法

异步测试最佳实践

  1. 分阶段验证:始终分两个阶段验证异步请求
  2. 超时设置:为 DeferredResult 设置合理的超时时间
  3. 错误处理:充分测试异步处理中的异常情况
  4. 资源清理:确保异步任务正确释放资源

❌ 常见陷阱

需要避免的问题

  • 忘记异步分发:只验证异步启动,忘记调用 asyncDispatch()
  • 超时时间过短:导致测试不稳定
  • 并发测试不当:在单元测试中进行过度的并发测试
  • 资源泄露:异步任务中的资源没有正确释放

总结

Spring MockMvc 的异步请求测试为我们提供了一套完整的测试方案,让我们能够:

🎯 全面验证异步处理流程

  • 验证异步启动
  • 检查异步结果
  • 确认最终响应

🚀 提升应用性能

  • 释放容器线程
  • 提高系统吞吐量
  • 改善用户体验

🔧 保证代码质量

  • 完整的测试覆盖
  • 异常情况处理
  • 性能基准验证

通过掌握这些异步测试技巧,你就能够自信地开发和测试现代化的高性能 Web 应用了! 🎉

NOTE

记住:异步不是银弹,但在正确的场景下使用异步处理,配合完善的测试,能够显著提升应用的性能和用户体验。