You need to enable JavaScript to run this app. Spring WebFlux Spock单元测试中Mono.map()内Mock类未被调用问题
优惠活动
大模型
产品
解决方案
定价
更多

Spring WebFlux Spock单元测试中Mono.map()内Mock类未被调用问题

问题产生的核心原因

这类问题是Reactor响应式开发结合Spock单测时非常常见的时序+类型叠加问题,具体拆解为两点:

  • 响应式流的惰性执行特性与Spock断言的执行顺序错位
    Reactor的核心设计是惰性组装:所有返回Flux/Mono的操作链,在没有收到订阅请求时,只会完成链的拓扑结构拼接,不会执行链上任何操作符的lambda逻辑。
    你的测试执行流程存在时序断层:
    1. 调用controller.getTransactions(id)仅拿到了组装完成的Flux实例,没有任何业务逻辑触发
    2. StepVerifier仅编写了消费逻辑的链式定义,没有调用终止方法(如verifyComplete())触发订阅、驱动信号在链上传导
    3. 直接进入Spock的then块校验mock调用次数时,整个响应式链还处于未激活状态,mapper.mapTransaction自然一次都没被调用,因此抛出“Too few invocations”异常
      这也是你在map lambda内部设置断点不会触发的根本原因:lambda体只有在订阅发生、信号传导到对应操作符时才会被执行,无订阅状态下断点永远不会命中。
  • 代码层面的类型不匹配
    你的控制器代码在Flux<Transaction>流上调用了map操作符,但从Spock桩的定义可以看到,mapper.mapTransaction的返回值是Flux<MappedTransaction>类型。
    map操作符仅支持将流中的单个元素T同步转换为单值R,不会自动拆解返回值为嵌套Publisher的结构。这种写法最终会得到Flux<Flux<MappedTransaction>>的嵌套流类型,和控制器方法声明的返回值不匹配。
对应修复方案
  1. 对齐测试执行时序,确保流完整执行后再做mock校验
    给StepVerifier的链式调用补充最终的验证方法,强制在进入Spock then块的mock校验前,完成全链路的订阅、信号传导、消费流程,保证所有内部逻辑的触发时机在校验之前。
  2. 修正类型不匹配问题
  • 如果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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.30 14:06:01