Search code examples
unit-testingmockingspock

Unit Testing a fluent API with mocking in Spock


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?


Solution

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