1

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?

1 Answer 1

3

Management summary

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.

Classes under test

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
  }
}

Test code

Testing the original class, no mocking

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"
  }
}

Simple stubbing without interaction testing

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"
  }

Mock with custom default response

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"
  }

Original answer

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"
  }
Sign up to request clarification or add additional context in comments.

2 Comments

Please note my update with a generic solution using à la carte mocks. This is quite elegant and works for your builder methods also if they do not have a common prefix like with, depending on the style of your fluent interface.
Update 2022-03-08: I re-wrote the answer after learning about the special behaviour of EmptyOrDummyResponse for methods returning the mocked type. I am also describing now the related Spock 2 syntactic sugar syntax.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.