Unit Testing a fluent API with mocking in Spock

邮差的信 提交于 2019-12-11 16:31:54

问题


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:


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.



来源:https://stackoverflow.com/questions/57298557/unit-testing-a-fluent-api-with-mocking-in-spock

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!