Kotlin1.5下Mockk1.12.0深度存根链式调用验证失败求解
报错的根因非常明确:
你代码的play方法中一共两次访问了notifier.action属性:第一次是调用notifier.action.getSteps()遍历步数,第二次是满足条件时调用notifier.action.send()发通知,对应Mockk层面就是触发了两次Notifier.getAction()调用。
而你直接写every { notifier.action.getSteps() }这种链式打桩语法时,Mockk会隐式为notifier.action生成一个临时mock实例,这个隐式打桩默认只允许1次action属性访问,第二次访问就会触发调用次数不匹配的断言错误。
方案1:显式Mock中间依赖(最稳定,推荐)
单独Mock Action实例,手动绑定到Notifier的action属性,避免Mockk生成临时隐式mock:
@ExtendWith(MockKExtension::class) class ServiceTest { @MockK private lateinit var rabbit: Rabbit @MockK private lateinit var action: Action // 单独声明Action的mock @MockK private lateinit var notifier: Notifier @InjectMockKs private lateinit var service: Service @Test fun `One notification should be sent`() { // 先给notifier的action属性打桩,返回我们单独mock的Action实例 every { notifier.action } returns action // 直接给Action实例的方法打桩,不需要写链式路径 every { action.getSteps() } returns listOf(1, 2, 3) every { rabbit.jump(1) } returns false every { rabbit.jump(2) } returns false every { rabbit.jump(3) } returns true every { action.send() } returns 1 service.play() verify(exactly = 3) { rabbit.jump(any()) } verify(exactly = 1) { action.send() } } }
这种方案适配你的RedisTemplate场景也非常方便:你可以单独Mock opsForSet()返回的SetOperations实例,先打桩every { redisTemplate.opsForSet() } returns 你mock的SetOperations实例,后续直接给SetOperations的members、remove等方法打桩即可。
方案2:开启Deep Stubs(代码改动最小)
给需要链式调用的Mock声明开启深度打桩,Mockk会自动复用链式调用中间节点的mock实例,允许多次访问:
// 给Notifier的Mock加上deepStubs = true参数 @MockK(deepStubs = true) private lateinit var notifier: Notifier
其余测试代码不需要改动,即可正常运行。这种方案适合链式层级多、中间节点不需要单独验证的场景,写起来更简洁。
方案3:链式打桩时绑定同一个mock实例
你也可以在打桩时直接把中间节点的返回指定为同一个mock,写法如下:
every { notifier.action } returns mockk { every { getSteps() } returns listOf(1,2,3) every { send() } returns 1 }
这种写法不需要额外声明Action的Mock变量,代码也比较紧凑。
内容的提问来源于stack exchange,提问作者Vahid

