本文详解如何在 spock 框架中精确断言被模拟对象(mock)的方法不仅被调用,而且按指定顺序、以指定参数被调用,避免仅验证调用次数导致的逻辑遗漏。
本文详解如何在 spock 框架中精确断言被模拟对象(mock)的方法不仅被调用,而且按指定顺序、以指定参数被调用,避免仅验证调用次数导致的逻辑遗漏。
在单元测试中,仅验证某个方法“被调用了两次”(如 2 * foo.bar(_))往往不足以保障业务逻辑的正确性——尤其是当调用顺序或参数依赖构成核心契约时(例如先初始化再提交、先写缓存再发通知)。Spock 提供了简洁而强大的交互式验证(interaction-based verification)机制,支持对方法调用的时序、参数、次数及副作用进行精细化断言。
✅ 正确做法:分段 then 块实现顺序断言
Spock 允许在单个测试方法中使用多个 then: 块,每个块独立验证一次交互,并隐式按出现顺序匹配实际调用流。这是验证调用顺序最直接、最可靠的方式:
def "myMethod calls foo.bar with 'a' then 'b' in order"() {
given:
def foo = Mock(Foo)
def myClass = new MyClass()
myClass.foo = foo // 注入 mock
when:
myClass.myMethod('a', 'b')
then:
1 * foo.bar('a') // 第一次调用必须传参 'a'
then:
1 * foo.bar('b') // 第二次调用必须传参 'b'
}⚠️ 注意:两个 then: 块之间不能插入其他动作(如 when: 或 given:),否则 Spock 将重置交互计数器,导致验证失效。
? 底层原理:Spock 的交互匹配是“有序累积”的
Spock 在执行 then: 块时,会按声明顺序依次从调用历史中消耗匹配的交互记录。第一个 then: 匹配最早发生的调用,第二个 then: 匹配其后紧接着的下一次调用。若实际调用顺序为 bar('b') → bar('a'),则上述测试将失败,并清晰提示:
Too few invocations for:
1 * foo.bar('a') (0 invocations)? 进阶技巧:组合验证更复杂场景
-
带副作用的顺序验证(如验证返回值或状态变更):
then: 1 * foo.bar('a') >> 'resultA' then: 1 * foo.bar('b') >> 'resultB' -
严格限定“仅此两次”且无额外调用(防意外冗余调用):
then: 1 * foo.bar('a') 1 * foo.bar('b') 0 * foo.bar(_) // 显式禁止任何其他参数的调用 -
使用 _ + 条件闭包校验参数结构(适用于对象参数):
1 * foo.processUser({ it.name == 'Alice' && it.age > 18 })
✅ 总结
| 目标 | 推荐方式 | 说明 |
|---|---|---|
| 验证调用顺序 + 参数 | 多 then: 块 | 简洁、语义明确、Spock 原生支持 |
| 仅验证次数 | n * foo.method(_) | 不保证顺序,慎用于有依赖逻辑的场景 |
| 防止意外调用 | 0 * foo.method(_) | 作为兜底断言,提升测试健壮性 |
通过合理运用多个 then: 块,你不仅能写出可读性强、意图清晰的测试,更能精准捕捉因重构或并发引发的时序缺陷——这正是高质量 Spock 测试的核心价值所在。

















