Unit Testing a fluent API with mocking in Spock

后端 未结 1 1172
天命终不由人
天命终不由人 2021-01-27 02:20

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 bra

相关标签:
1条回答
  • 2021-01-27 02:49

    You need to make sure that your builder mock's stubbed with* methods return the mock itself and the build() method returns whatever object (real or also mock) you want.

    How about this? The first feature method is just for illustration, you are interested in the second and third one. Please note that with the with* methods returning the mock object you cannot define the stubs inline (i.e. Mock() { myMethod(_) >> myResult }), for build() you could because it does not reference the mock object itself.

    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"
      }
    
      def "create person with mock builder, no interactions"() {
        given:
        def personBuilder = Mock(PersonBuilder)
        personBuilder./with.*/(_) >> personBuilder
        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"
      }
    
      def "create person with mock builder, use interactions"() {
        given:
        def personBuilder = Mock(PersonBuilder)
    
        when:
        def person = personBuilder
          .withHair("blonde")
          .withAge(22)
          .withName("Alice")
          .build()
    
        then:
        3 * personBuilder./with.*/(_) >> personBuilder
        1 * personBuilder.build() >> new Person(name: "John Doe", age: 99, hair: "black")
        person.age == 99
        person.hair == "black"
        person.name == "John Doe"
      }
    }
    

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

    Update: 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 add these two methods to the Spock specification above and see how you can create à la carte mocks both with or without interactions and easily define all stubs and interactions inline:

      def "create person with a la carte mock builder, no interactions"() {
        given:
        PersonBuilder personBuilder = Mock(defaultResponse: ThisResponse.INSTANCE) {
          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"
      }
    
      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"
      }
    

    Update 2: This sample test does not really make much sense because it just tests the mock, not any application code. But my approach comes in handy if you actually inject a mock like this into an object using it as a dependency. And the more I think about it the more I like the à la carte mock with the custom IDefaultResponse because it can be used generically for fluent API classes.

    0 讨论(0)
提交回复
热议问题