在Spock中通过模拟对流利的API进行单元测试 [英] Unit Testing a fluent API with mocking in Spock

查看:85
本文介绍了在Spock中通过模拟对流利的API进行单元测试的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Spock在Stub和Mock之间有很强的区别.当要更改的内容从被测类使用的类返回时,请使用存根,以便您可以测试if语句的另一个分支.使用一个模拟程序,当您不在乎被测类返回什么内容时,只需调用另一个类的另一个方法即可,并且您要确保已调用了该方法.非常整洁.但是,假设您有一个使用能使人流利的API的构建器.您想测试一种调用此Builder的方法.

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

因此,最初,我只是想模拟构建器,然后对myMethod()进行单元测试时应使用正确的参数来检查withAge(),withHair().

So originally, I was thinking just mock the builder and then the unit test for myMethod() should check withAge(), withHair() with the right parameters.

太酷了.

但是-模拟方法返回null.这意味着您不能使用流畅的API.

However -- the mock methods return null. Meaning you can't use the fluent API.

你可以的.

Person myMethod(int age) {
     ...
     // do stuff
     ...

     builder.withAge(age);
     builder.withHair("brown");
     builder.withName("tony");
     builder.build();
     return tony; 
}

有效.您的测试可以正常工作,但无法达到使用流畅API的目的.

which works. You test will work but it defeats the purpose of using the fluent API.

那么,如果您使用的是流畅的API,那么您会进行存根或模拟还是其他?

So, if you are using fluent APIs, do you stub or mock or what?

推荐答案

您需要确保构建器模拟的存根with*方法返回模拟本身,而build()方法返回任何对象(真实或模拟)你想要的.

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.

这个怎么样?第一个特征方法仅用于说明,您对第二个和第三个特征感兴趣.请注意,使用with*方法返回模拟对象时,您无法内联定义存根(即Mock() { myMethod(_) >> myResult }),对于build()而言,您可以定义,因为它未引用模拟对象本身.

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


更新:如果您想要构建器类的通用解决方案,则可以使用点菜模拟.注意:在创建模拟时,手册指定了一个自定义的IDefaultResponse类型参数,但您需要指定该类型的实例.


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.

在这里,我们有自定义的IDefaultResponse,它使模拟调用的默认响应不是null,零或空对象,而是模拟实例本身.这是模拟具有流畅接口的构建器类的理想选择.您只需要确保对build()方法存根即可实际返回要构建的对象,而不是模拟对象.例如,PersonBuilder.build()不应返回默认的PersonBuilder模拟,而应返回Person.

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

现在将这两种方法添加到上面的Spock规范中,看看如何创建具有交互作用或没有交互作用的点菜模拟,并轻松地内联定义所有存根和交互作用:

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


更新2:该示例测试实际上没有多大意义,因为它仅测试模拟而不是任何应用程序代码.但是,如果您将这样的模拟实际上作为对象依赖项注入到对象中,则我的方法会派上用场.而且我考虑得越多,就越喜欢使用自定义IDefaultResponse的点菜"模拟,因为它可以通用地用于流利的API类.


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.

这篇关于在Spock中通过模拟对流利的API进行单元测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆