1. Introduction
When we test our code, we sometimes want to capture the parameters passed to our method.
In this tutorial, we’ll learn how to capture arguments in a Spock test using Stubs, Mocks, and Spies and check what we captured. We’ll also learn how to verify multiple calls to the same Mock with different arguments and assert the order of those calls.
2. The Subject of Our Test
First, we need a method that takes a single parameter or argument we want to capture.
So, let’s create an ArgumentCaptureSubject with a catchMeIfYouCan() method that takes a String and returns it with “Received ” prepended:
public class ArgumentCaptureSubject {
public String catchMeIfYouCan(String input) {
return "Received " + input;
}
}
3. Preparing Our Data-Driven Test
We’ll start our tests with the typical use of a Stub and evolve it to capture arguments.
Let’s create a Stub of our class to return a stubbed response of “42” and invoke its catchMeIfYouCan() method:
def "given a Stub when we invoke it then we capture the stubbed response"() {
given: "an input and a result"
def input = "Input"
def stubbedResponse = "42"
and: "a Stub for our response"
@Subject
ArgumentCaptureSubject stubClass = Stub()
stubClass.catchMeIfYouCan(_) >> stubbedResponse
when: "we invoke our Stub's method"
def result = stubClass.catchMeIfYouCan(input)
then: "we get our stubbed response"
result == stubbedResponse
}
We’ve used a simple Stub in this example as we’re not verifying any method invocations.
4. Capturing Arguments
Now we have our basic test, let’s see how to capture the argument we used to invoke our method.
First, we’ll declare a method-scoped variable to assign when we capture the argument:
def captured
Next, we’ll replace our static stubbedResponse with a Groovy Closure. Spock will pass our Closure a List of method arguments when our stubbed method is invoked.
Let’s create a simple Closure to capture the arguments list and assign it to our captured variable:
{ arguments -> captured = arguments }
For our assertion, we’ll assert that the first element in our list of captured arguments, at index 0, equals our input:
captured[0] == input
So, let’s update our test with our captured variable declaration, replace our stubbedResponse with our argument-capturing Closure, and add our assertion:
def "given a Stub when we invoke it then we capture the argument"() {
given: "an input"
def input = "Input"
and: "a variable and a Stub with a Closure to capture our arguments"
def captured
@Subject
ArgumentCaptureSubject stubClass = Stub()
stubClass.catchMeIfYouCan(_) >> { arguments -> captured = arguments }
when: "we invoke our method"
stubClass.catchMeIfYouCan(input)
then: "we captured the method argument"
captured[0] == input
}
When we want to return a stubbedResponse as well as capture the arguments, we update our Closure to return it:
{ arguments -> captured = arguments; return stubbedResponse }
...
then: "what we captured matches the input and we got our stubbed response"
captured == input
result == stubbedResponse
Note that although we used “return” for clarity, it isn’t strictly necessary since Groovy closures return the result of the last executed statement by default.
When we’re only interested in capturing one of the arguments, we can capture the argument we want by using its index in the Closure:
{ arguments -> captured = arguments[0] }
...
then: "what we captured matches the input"
captured == input
In this case, our captured variable will be the same type as our parameter – a String.
5. Capturing With Spies
*When we want to capture a value but also want the method to continue executing, we add a call to Spy‘s callRealMethod().*
Let’s update our test to use a Spy instead of a Stub and use the Spy‘s callRealMethod() in our Closure:
def "given a Spy when we invoke it then we capture the argument and then delegate to the real method"() {
given: "an input string"
def input = "Input"
and: "a variable and a Spy with a Closure to capture the first argument and call the underlying method"
def captured
@Subject
ArgumentCaptureSubject spyClass = Spy()
spyClass.catchMeIfYouCan(_) >> { arguments -> captured = arguments[0]; callRealMethod() }
when: "we invoke our method"
def result = spyClass.catchMeIfYouCan(input)
then: "what we captured matches the input and our result comes from the real method"
captured == input
result == "Received Input"
}
Here, we’ve captured the input argument without impacting the method’s return value.
When we want to change the captured argument before passing it on to the real method, we update it inside the closure, then use Spy’s callRealMethodWithArgs to pass on our updated argument.
So, let’s update our Closure to prepend “Tampered: ” to our String before passing it on to the real method:
spyClass.catchMeIfYouCan(_) >> { arguments -> captured = arguments[0]; callRealMethodWithArgs('Tampered:' + captured) }
And let’s update our assertion to expect our tampered result:
result == "Received Tampered:Input"
6. Capturing Arguments Using an Injected Mock
Now that we’ve seen how to use Spock’s mocking framework to capture an argument, let’s apply this technique to a class with a dependency that we can mock.
First, let’s create an ArgumentCaptureDependency class that our subject can call with a simple catchMe() method that takes and modifies a String:
public class ArgumentCaptureDependency {
public String catchMe(String input) {
return "***" + input + "***";
}
}
Now, let’s update our ArgumentCaptureSubject with a constructor that takes our ArgumentCaptureDependency. Let’s also add a callOtherClass method that takes no parameters and calls our ArgumentCaptureDependency‘s catchMe() method with a parameter:
public class ArgumentCaptureSubject {
ArgumentCaptureDependency calledClass;
public ArgumentCaptureSubject(ArgumentCaptureDependency calledClass) {
this.calledClass = calledClass;
}
public String callOtherClass() {
return calledClass.catchMe("Internal Parameter");
}
}
Finally, let’s create a test like before. This time, let’s inject a Spy into our ArgumentCaptureSubject when we create it so that we can also callRealMethod() and compare the result:
def "given an internal method call when we invoke our subject then we capture the internal argument and return the result of the real method"() {
given: "a mock and a variable for our captured argument"
ArgumentCaptureDependency spyClass = Spy()
def captured
spyClass.catchMe(_) >> { arguments -> captured = arguments[0]; callRealMethod() }
and: "our subject with an injected Spy"
@Subject argumentCaptureSubject = new ArgumentCaptureSubject(spyClass)
when: "we invoke our method"
def result = argumentCaptureSubject.callOtherClass(input)
then: "what we captured matches the internal method argument"
captured == "Internal Parameter"
result == "***Internal Parameter***"
}
Our test captured the internal argument “Internal Parameter“. In addition, our invocation of Spy‘s callRealMethod ensured that we didn’t impact the result of the method: “***Internal Parameter***”.
When we don’t need to return the real result, we can simply use a Stub or Mock.
Note that when we test Spring applications, we can inject our Mock using Spock’s @SpringBean annotation.
7. Capturing Arguments From Multiple Invocations
Sometimes, our code invokes a method multiple times, and we want to capture the value for each invocation.
So, let’s add a String parameter to the callOtherClass() method in our ArgumentCaptureSubject. We’ll invoke this with different parameters and capture them.
public String callOtherClass(String input) {
return calledClass.catchMe(input);
}
We need a collection to capture the argument from each invocation. So, we’ll declare a capturedStrings variable as an ArrayList:
def capturedStrings = new ArrayList()
Now, let’s create our test and make it invoke our callOtherClass() twice, first with “First” as a parameter and then with “Second”:
def "given an dynamic Mock when we invoke our subject then we capture the argument for each invocation"() {
given: "a variable for our captured arguments and a mock to capture them"
def capturedStrings = new ArrayList()
ArgumentCaptureDependency mockClass = Mock()
and: "our subject"
@Subject argumentCaptureSubject = new ArgumentCaptureSubject(mockClass)
when: "we invoke our method"
argumentCaptureSubject.callOtherClass("First")
argumentCaptureSubject.callOtherClass("Second")
}
Now, let’s add a Closure to our Mock to capture the argument from each invocation and add it to our list. Let’s also have our Mock verify that our method was called twice by prefixing “2 *” to our statement:
then: "our method was called twice and captured the argument"
2 * mockClass.catchMe(_ as String) >> { arguments -> capturedStrings.add(arguments[0]) }
Finally, let’s assert that we captured both arguments in the right order:
and: "we captured the list and it contains an entry for both of our input values"
capturedStrings[0] == "First"
capturedStrings[1] == "Second"
When we’re not concerned about the order, we can use List‘s contains method:
capturedStrings.contains("First")
8. Using Multiple Then Blocks
Sometimes, we want to assert a sequence of calls using the same method with different arguments but don’t need to capture them. Spock allows verifications in any order within the same then block, so it won’t matter what order we write them in. We can, however, enforce the order by adding multiple then blocks.
Spock verifies that assertions in one then block are met before assertions in the next then block.
So, let’s add two then blocks to verify that our method is invoked with the correct arguments in the right sequence:
def "given a Mock when we invoke our subject twice then our Mock verifies the sequence"() {
given: "a mock"
ArgumentCaptureDependency mockClass = Mock()
and: "our subject"
@Subject argumentCaptureSubject = new ArgumentCaptureSubject(mockClass)
when: "we invoke our method"
argumentCaptureSubject.callOtherClass("First")
argumentCaptureSubject.callOtherClass("Second")
then: "we invoked our Mock with 'First' the first time"
1 * mockClass.catchMe( "First")
then: "we invoked our Mock with 'Second' the next time"
1 * mockClass.catchMe( "Second")
}
When our invocations occur in the wrong order, such as when we invoke callOtherClass(“Second”) first, Spock gives us a helpful message:
Wrong invocation order for:
1 * mockClass.catchMe( "First") (1 invocation)
Last invocation: mockClass.catchMe('First')
Previous invocation:
mockClass.catchMe('Second')
9. Conclusion
In this tutorial, we learned how to capture method arguments using Spock’s Stubs, Mocks, and Spies using Closures. Next, we learned how to use a Spy to change a captured argument before invoking the real method. We also learned how to collect arguments when our method is invoked multiple times. Finally, as an alternative to capturing the argument, we learned how to use multiple then blocks to check that our invocations occurred in the right sequence.
As usual, the source for this article can be found over on GitHub.