Spock makes a strong distinction between a Stub and Mock. Use a stub when what to change want comes back from a class your class under test uses so that you can test another branch of an if statement. Use a mock, when you don't care what comes back your class under test just call another method of another class and you want to ensure you called that. It's very neat. However suppose you have a builder with a fluent API that makes people. You want to test a method that calls this Builder.
Person myMethod(int age) {
...
// do stuff
...
Person tony =
builder.withAge(age).withHair("brown").withName("tony").build();
return tony;
}
So originally, I was thinking just mock the builder and then the unit test for myMethod() should check withAge(), withHair() with the right parameters.
All cool.
However -- the mock methods return null. Meaning you can't use the fluent API.
You could do.
Person myMethod(int age) {
...
// do stuff
...
builder.withAge(age);
builder.withHair("brown");
builder.withName("tony");
builder.build();
return tony;
}
which works. You test will work but it defeats the purpose of using the fluent API.
So, if you are using fluent APIs, do you stub or mock or what?
If you do not need to verify interactions like 1 * myMock.doSomething("foo")
, you can use a Stub
instead of a Mock
, because while mocks always return null
, false
or 0
, stubs return a more sophisticated default response, e.g. empty objects rather than null
and - most importantly - the stub itself for methods with a return type matching the stubbed type. I.e., testing fluent APIs with stubs is easy.
If however you wish to also verify interactions, you cannot use a Stub
and have to use a Mock
instead. But there the default response is null
, i.e. you need to override it for the fluent API methods. This is quite easy in both In Spock 1.x and 2.x. Specifically in 2.x, there is some syntactic sugar for it, making the code even smaller.
Quick & dirty implementation, just for illustration:
package de.scrum_master.stackoverflow.q57298557
import groovy.transform.ToString
@ToString(includePackage = false)
class Person {
String name
int age
String hair
}
package de.scrum_master.stackoverflow.q57298557
class PersonBuilder {
Person person = new Person()
PersonBuilder withAge(int age) {
person.age = age
this
}
PersonBuilder withName(String name) {
person.name = name
this
}
PersonBuilder withHair(String hair) {
person.hair = hair
this
}
Person build() {
person
}
}
package de.scrum_master.stackoverflow.q57298557
import spock.lang.Specification
class PersonBuilderTest extends Specification {
def "create person with real builder"() {
given:
def personBuilder = new PersonBuilder()
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
person.age == 22
person.hair == "blonde"
person.name == "Alice"
}
}
This is the simple case and works for both Spock 1.x and 2.x. Add this feature method to your Spock specification:
def "create person with stub builder, no interactions"() {
given:
PersonBuilder personBuilder = Stub()
personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
Just tell Spock to use stub-like default responses for your mock:
import org.spockframework.mock.EmptyOrDummyResponse
// ...
def "create person with mock builder, use interactions"() {
given:
PersonBuilder personBuilder = Mock(defaultResponse: EmptyOrDummyResponse.INSTANCE)
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
3 * personBuilder./with.*/(_)
1 * personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
The syntax above works for bork Spock 1.x and 2.x. Since version 2.0-M3, Spock users can tell their mocks/spies to return a stub-like default response using the syntactic sugar syntax >> _
, e.g. in the simplest case
Mock() {
_ >> _
}
Thanks to Spock maintainer Leonard Brünings for sharing this neat little trick.
Then later in the then:
or expect:
block, you can still define additional interactions and stub responses, overriding the default. In your case, it could look like this:
import spock.lang.Requires
import org.spockframework.util.SpockReleaseInfo
//...
@Requires({ SpockReleaseInfo.version.major >= 2})
def "create person with mock builder, use interactions, Spock 2.x"() {
given:
PersonBuilder personBuilder = Mock()
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
3 * personBuilder./with.*/(_) >> _
1 * personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
person.age == 99
person.hair == "black"
person.name == "John Doe"
}
Before I realised that Spock's own EmptyOrDummyResponse
, which is used by stubs by default, actually returns the mock instance for methods matching with a return type matching the mocked/stubbed class, I thought it would just return an empty object like for methods with other return types, i.e. empty strings, collections etc. Therefore, I invented my own ThisResponse
type. Even though it is not necessary here, I am keeping the old solution, because it teaches users how to implement and use custom default responses.
If you want a generic solution for builder classes, you can use à la carte mocks as described in the Spock manual. A little caveat: The manual specifies a custom IDefaultResponse
type parameter when creating the mock, but you need to specify an instance of that type instead.
Here we have our custom IDefaultResponse
which makes the default response for mock calls not null, zero or an empty object, but the mock instance itself. This is ideal for mocking builder classes with fluent interfaces. You just need to make sure to stub the build()
method to actually return the object to be built, not the mock. For example, PersonBuilder.build()
should not return the default PersonBuilder
mock but a Person
.
package de.scrum_master.stackoverflow.q57298557
import org.spockframework.mock.IDefaultResponse
import org.spockframework.mock.IMockInvocation
class ThisResponse implements IDefaultResponse {
public static final ThisResponse INSTANCE = new ThisResponse()
private ThisResponse() {}
@Override
Object respond(IMockInvocation invocation) {
invocation.mockObject.instance
}
}
Now you can use ThisResponse
in your mocks as follows:
def "create person with a la carte mock builder, use interactions"() {
given:
PersonBuilder personBuilder = Mock(defaultResponse: ThisResponse.INSTANCE) {
3 * /with.*/(_)
1 * build() >> new Person(name: "John Doe", age: 99, hair: "black")
}
when:
def person = personBuilder
.withHair("blonde")
.withAge(22)
.withName("Alice")
.build()
then:
person.age == 99
person.hair == "black"
person.name == "John Doe"
}