1. 引言

在测试代码时,我们有时需要捕获传递给方法的参数。本文将介绍如何使用Spock框架中的StubMockSpy来捕获参数,并验证捕获结果。我们还会学习如何验证同一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. 捕获参数

现在改造测试以捕获方法参数。分三步实现:

  1. 声明捕获变量

    def captured
    
  2. 用闭包替换固定响应

    stubClass.catchMeIfYouCan(_) >> { arguments -> captured = arguments }
    

    ⚠️ Spock调用闭包时会传入参数列表arguments

  3. 验证捕获结果

    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仓库查看。


原始标题:Capturing Method Arguments When Running Spock Tests | Baeldung