1. 引言
在测试代码时,我们有时需要捕获传递给方法的参数。本文将介绍如何使用Spock框架中的Stub、Mock和Spy来捕获参数,并验证捕获结果。我们还会学习如何验证同一Mock的多次调用及其调用顺序。
2. 测试目标
首先需要一个包含待捕获参数的方法。创建ArgumentCaptureSubject
类,其catchMeIfYouCan()
方法接收字符串并返回添加前缀的结果:
public class ArgumentCaptureSubject {
public String catchMeIfYouCan(String input) {
return "Received " + input;
}
}
3. 准备数据驱动测试
从基础的Stub用法开始,逐步扩展到参数捕获。创建Stub并设置固定返回值"42":
def "使用Stub时捕获固定响应"() {
given: "准备输入和预期结果"
def input = "Input"
def stubbedResponse = "42"
and: "创建Stub"
@Subject
ArgumentCaptureSubject stubClass = Stub()
stubClass.catchMeIfYouCan(_) >> stubbedResponse
when: "调用方法"
def result = stubClass.catchMeIfYouCan(input)
then: "验证固定响应"
result == stubbedResponse
}
✅ 这里使用Stub是因为不需要验证方法调用行为
4. 捕获参数
现在改造测试以捕获方法参数。分三步实现:
声明捕获变量:
def captured
用闭包替换固定响应:
stubClass.catchMeIfYouCan(_) >> { arguments -> captured = arguments }
⚠️ Spock调用闭包时会传入参数列表
arguments
验证捕获结果:
captured[0] == input
完整测试代码:
def "使用Stub捕获方法参数"() {
given: "准备输入"
def input = "Input"
and: "声明捕获变量和配置Stub"
def captured
@Subject
ArgumentCaptureSubject stubClass = Stub()
stubClass.catchMeIfYouCan(_) >> { arguments -> captured = arguments }
when: "调用方法"
stubClass.catchMeIfYouCan(input)
then: "验证捕获的参数"
captured[0] == input
}
高级技巧
- 同时返回固定值:
{ arguments -> captured = arguments; return stubbedResponse }
- 直接捕获特定参数(避免索引访问):
{ arguments -> captured = arguments[0] }
5. 使用Spy捕获参数
当需要保留原始方法行为时,改用Spy并调用callRealMethod()
:
def "使用Spy捕获参数并保留原始行为"() {
given: "准备输入"
def input = "Input"
and: "配置Spy"
def captured
@Subject
ArgumentCaptureSubject spyClass = Spy()
spyClass.catchMeIfYouCan(_) >> {
arguments ->
captured = arguments[0]
callRealMethod()
}
when: "调用方法"
def result = spyClass.catchMeIfYouCan(input)
then: "验证参数和结果"
captured == input
result == "Received Input"
}
参数篡改场景
在调用真实方法前修改参数:
spyClass.catchMeIfYouCan(_) >> {
arguments ->
captured = arguments[0]
callRealMethodWithArgs('Tampered:' + captured)
}
验证篡改结果:
result == "Received Tampered:Input"
6. 通过注入Mock捕获参数
现在扩展到带依赖的场景。首先创建依赖类:
public class ArgumentCaptureDependency {
public String catchMe(String input) {
return "***" + input + "***";
}
}
修改主类注入依赖:
public class ArgumentCaptureSubject {
ArgumentCaptureDependency calledClass;
public ArgumentCaptureSubject(ArgumentCaptureDependency calledClass) {
this.calledClass = calledClass;
}
public String callOtherClass() {
return calledClass.catchMe("Internal Parameter");
}
}
测试代码:
def "通过注入Mock捕获内部参数"() {
given: "配置Spy和捕获变量"
ArgumentCaptureDependency spyClass = Spy()
def captured
spyClass.catchMe(_) >> {
arguments ->
captured = arguments[0]
callRealMethod()
}
and: "创建主类实例"
@Subject argumentCaptureSubject = new ArgumentCaptureSubject(spyClass)
when: "调用方法"
def result = argumentCaptureSubject.callOtherClass()
then: "验证内部参数和结果"
captured == "Internal Parameter"
result == "***Internal Parameter***"
}
💡 在Spring应用中可用
@SpringBean
注入Mock
7. 捕获多次调用的参数
当方法被多次调用时,使用列表收集所有参数:
def "捕获多次调用的参数"() {
given: "准备捕获列表和Mock"
def capturedStrings = new ArrayList()
ArgumentCaptureDependency mockClass = Mock()
and: "创建主类实例"
@Subject argumentCaptureSubject = new ArgumentCaptureSubject(mockClass)
when: "多次调用方法"
argumentCaptureSubject.callOtherClass("First")
argumentCaptureSubject.callOtherClass("Second")
then: "验证调用次数并捕获参数"
2 * mockClass.catchMe(_ as String) >> {
arguments ->
capturedStrings.add(arguments[0])
}
and: "验证捕获顺序"
capturedStrings[0] == "First"
capturedStrings[1] == "Second"
}
不关心顺序时
capturedStrings.contains("First")
8. 使用多个Then块验证顺序
当需要严格验证调用顺序时,使用多个then
块:
def "使用多个Then块验证调用顺序"() {
given: "创建Mock"
ArgumentCaptureDependency mockClass = Mock()
and: "创建主类实例"
@Subject argumentCaptureSubject = new ArgumentCaptureSubject(mockClass)
when: "按顺序调用方法"
argumentCaptureSubject.callOtherClass("First")
argumentCaptureSubject.callOtherClass("Second")
then: "验证第一次调用"
1 * mockClass.catchMe("First")
then: "验证第二次调用"
1 * mockClass.catchMe("Second")
}
顺序错误时的提示
当调用顺序错误时,Spock会给出明确提示:
Wrong invocation order for:
1 * mockClass.catchMe("First") (1 invocation)
Last invocation: mockClass.catchMe('First')
Previous invocation:
mockClass.catchMe('Second')
9. 总结
本文介绍了Spock测试中捕获参数的核心技巧:
- ✅ 使用Stub/闭包捕获基础参数
- ✅ 通过Spy保留原始方法行为
- ✅ 在依赖注入场景捕获内部参数
- ✅ 处理多次调用的参数收集
- ✅ 用多个
then
块验证调用顺序
这些技巧能帮我们更精准地验证方法交互行为,避免测试中的"踩坑"。完整代码示例可在GitHub仓库查看。