当前位置: 首页 > 知识库问答 >
问题:

在Spock中对fluent API进行模拟的单元测试

施阳曜
2023-03-14

Spock对存根和模拟做了很强的区分。当要更改的内容从被测试类使用的类返回时,请使用存根,这样您就可以测试if语句的另一个分支。使用mock,当您不关心测试中的类返回什么时,只需调用另一个类的另一个方法,并且您希望确保调用了该方法。很整洁。然而,假设您有一个具有流利API的构建器,它使人们。您希望测试调用此生成器的方法。

Person myMethod(int age) {
     ...
     // do stuff
     ...
     Person tony = 
            builder.withAge(age).withHair("brown").withName("tony").build();
     return tony; 
}

所以最初,我想只是模拟构建器,然后myMethod()的单元测试应该检查具有正确参数的withAge()、withHair()。

都很酷。

但是mock方法返回NULL。这意味着您不能使用fluent API。

你可以做的。

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

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

共有1个答案

韶兴德
2023-03-14

您需要确保生成器模拟的stubbedwith*方法返回模拟本身,并且build()方法返回所需的任何对象(真实的或也是模拟的)。

这样怎么样?第一个特征方法只是为了说明,你对第二个和第三个感兴趣。请注意,对于返回模拟对象的with*方法,您不能内联定义存根(即mock(){myMethod(_)>>myResult}),而对于build(),您可以定义存根,因为它没有引用模拟对象本身。

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

测试中的类(快速和肮脏的实现,仅供说明):

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

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

这里有自定义的idefaultresponse,它使模拟调用的默认响应不是null、0或空对象,而是模拟实例本身。这对于模拟具有流畅接口的构建器类非常理想。您只需要确保存根build()方法,以实际返回要构建的对象,而不是模拟。例如,PersonBuilder.build()不应返回默认的PersonBuilder模拟,而应返回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
  }
}
  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类。

 类似资料:
  • 我有一个示例方法(我需要编写测试用例)如下所示, 我想模拟getConfig方法并返回一个特定的字符串值。getConfig是Kotlin对象中方法,如下所示, 下面是我尝试的测试 我没有得到任何错误,但是getConfig方法没有被嘲笑。执行实际的实现。我也试过使用Powermockito。请帮帮我

  • 问题内容: 我正在尝试为一些依赖WifiManager和返回的ScanResults的类实现一些单元测试。我想做的是能够控制我收到的ScanResults,以测试各种不同的条件。 不幸的是,对我来说,成功模拟WifiManager非常困难(尽管我想我可以在MockWifiManager中传递其构造函数null引用)。这只是我的第一个问题,因为一旦我有一个MockWifiManager可以玩(如果它

  • 在如何模拟Grails单元测试中使用的自动有线依赖方面,我可以提供一些建议。我省略了大部分不必要的代码,只给出了测试类和被测试文件类中的相关方法 如果不对此依赖性进行攻击或嘲弄,我就会得到错误 我尝试存根密码编码器并让它返回true 但这会给出一条错误消息: 有什么方法可以用Spock来嘲笑这种依赖吗?

  • 我在尝试包装我的代码以用于单元测试时遇到了一些问题。问题是。我有接口IHttpHandler: 现在很明显,我将在Connection类中有一些方法,这些方法将从my后端检索数据(JSON)。但是,我想为这个类编写单元测试,显然我不想编写针对真实后端的测试,而是一个被嘲弄的测试。我曾尝试谷歌一个很好的答案,但没有很大的成功。我以前可以并且曾经使用过Moq来模拟,但是从来没有在像HttpClient

  • 遇到了另一个常见的问题,同时为Spring Batch编写单元测试和集成测试组件是如何模拟域对象。一个很好的例子是StepExecutionListener,如下所示: public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport { public ExitStatus afterSte

  • 我正在寻找一种方法来模拟Controller中使用的服务bean,这样我就可以使用MockMVC只测试Controller。但是我找不到一个简单的方法来用Spock Mock代替real bean。一切都使用spring-boot 1.3.2版本。更多细节如下: 我有一个以下控制器类 和集成Spock测试: 我需要一种方法来替换这个autowired bean,用一个mock/stub这样我就可以