Spring WebFlux Spock单元测试中Mono.map()内Mock类未被调用问题
问题产生的核心原因
这类问题是Reactor响应式开发结合Spock单测时非常常见的时序+类型叠加问题,具体拆解为两点:
- 响应式流的惰性执行特性与Spock断言的执行顺序错位
Reactor的核心设计是惰性组装:所有返回Flux/Mono的操作链,在没有收到订阅请求时,只会完成链的拓扑结构拼接,不会执行链上任何操作符的lambda逻辑。
你的测试执行流程存在时序断层:- 调用
controller.getTransactions(id)仅拿到了组装完成的Flux实例,没有任何业务逻辑触发 - StepVerifier仅编写了消费逻辑的链式定义,没有调用终止方法(如
verifyComplete())触发订阅、驱动信号在链上传导 - 直接进入Spock的then块校验mock调用次数时,整个响应式链还处于未激活状态,
mapper.mapTransaction自然一次都没被调用,因此抛出“Too few invocations”异常
这也是你在maplambda内部设置断点不会触发的根本原因:lambda体只有在订阅发生、信号传导到对应操作符时才会被执行,无订阅状态下断点永远不会命中。
- 调用
- 代码层面的类型不匹配
你的控制器代码在Flux<Transaction>流上调用了map操作符,但从Spock桩的定义可以看到,mapper.mapTransaction的返回值是Flux<MappedTransaction>类型。map操作符仅支持将流中的单个元素T同步转换为单值R,不会自动拆解返回值为嵌套Publisher的结构。这种写法最终会得到Flux<Flux<MappedTransaction>>的嵌套流类型,和控制器方法声明的返回值不匹配。
对应修复方案
- 对齐测试执行时序,确保流完整执行后再做mock校验
给StepVerifier的链式调用补充最终的验证方法,强制在进入Spock then块的mock校验前,完成全链路的订阅、信号传导、消费流程,保证所有内部逻辑的触发时机在校验之前。 - 修正类型不匹配问题
- 如果
TransactionMapper.mapTransaction是同步处理方法,入参为单个Transaction、返回单个MappedTransaction对象,则调整测试中的mock返回值,去掉多余的Flux包装,匹配map操作符的单值转换要求 - 如果
mapTransaction本身是异步方法,返回Mono/Flux类型,则将控制器中的map替换为flatMap/flatMapMany,自动拆平嵌套的流结构,匹配控制器方法的返回类型声明
修正后的参考代码
控制器侧修正(适配mapper返回Flux的异步场景)
@RestController @RequestMapping("/v1/transactions") class TransactionController { @Autowired TransactionService service @Autowired TransactionMapper mapper Flux<MappedTransaction> getTransactions(@PathVariable String id) { service.getTransactions(id) .flatMapMany({ mapper.mapTransaction(it) }) // 替换map为flatMapMany,拆平嵌套流 } }
测试侧修正(同步mapper场景)
class TransactionControllerSpec extends Specification { def mockService = Mock(TransactionService) def mockMapper = Mock(TransactionMapper) def controller = new TransactionController(service: mockService, mapper: mockMapper) def "should call the transactions service to fetch a list of transactions"() { given: "preset mock return values" def id = "123" mockService.getTransactions(id) >> Flux.just(new Transaction()) mockMapper.mapTransaction(_) >> new MappedTransaction() when: "trigger full flow verification" def result = controller.getTransactions(id) StepVerifier.create(result) .consumeNextWith({ /* 业务断言逻辑 */ }) .verifyComplete() // 触发全链路执行直到流终止 then: "mock invocations meet expectations" 1 * mockService.getTransactions(id) 1 * mockMapper.mapTransaction(_) } }
内容的提问来源于stack exchange,提问作者jndietz
相关产品推荐
相关产品推荐

