Spock测试框架中Mock / Stub / Spy之间的区别

Posted

技术标签:

【中文标题】Spock测试框架中Mock / Stub / Spy之间的区别【英文标题】:Difference between Mock / Stub / Spy in Spock test framework 【发布时间】:2014-08-16 06:47:26 【问题描述】:

我不明白 Spock 测试中 Mock、Stub 和 Spy 之间的区别,并且我一直在网上查看的教程没有详细解释它们。

【问题讨论】:

【参考方案1】:

注意:我将在接下来的段落中过度简化,甚至可能会稍微伪造。如需更多详细信息,请参阅Martin Fowler's website。

mock 是一个替代真实类的虚拟类,每个方法调用都返回 null 或 0 之类的值。如果你需要一个复杂类的虚拟实例,你可以使用一个模拟实例,否则它会使用外部资源,如网络连接、文件或数据库,或者可能使用许多其他对象。模拟的优点是您可以将被测类与系统的其余部分隔离开来。

存根也是一个虚拟类,为某些被测请求提供一些更具体的、准备好的或预先记录的、重放的结果。你可以说存根是一个花哨的模拟。在 Spock 中,您会经常阅读到 stub 方法。

间谍是真实对象和存根之间的混合体,即它基本上是真实对象,其中一些(不是全部)方法被存根方法所掩盖。非存根方法只是路由到原始对象。这样,您可以对“便宜”或琐碎的方法有原始行为,而对“昂贵”或复杂的方法有虚假行为。


2017-02-06 更新: 实际上用户 mikhail 的回答比我上面的原始回答更具体到 Spock。所以在 Spock 的范围内,他的描述是正确的,但这并不能证伪我的一般回答:

存根与模拟特定行为有关。在 Spock 中,这是 stub 所能做的一切,所以这是最简单的事情。 mock 与代表(可能很昂贵的)真实对象有关,为所有方法调用提供无操​​作答案。在这方面,模拟比存根更简单。但在 Spock 中,mock 也可以 stub 方法结果,即既是 mock 又是 stub。此外,在 Spock 中,我们可以计算在测试期间调用具有特定参数的特定模拟方法的频率。 间谍总是包装一个真实的对象,默认情况下将所有方法调用路由到原始对象,也传递原始结果。方法调用计数也适用于间谍。在 Spock 中,间谍还可以修改原始对象的行为、操纵方法调用参数和/或结果或完全阻止调用原始方法。

现在这里是一个可执行的示例测试,展示什么是可能的,什么是不可能的。它比 mikhail 的 sn-ps 更有指导意义。非常感谢他激励我改进自己的答案! :-)

package de.scrum_master.***

import org.spockframework.mock.TooFewInvocationsError
import org.spockframework.runtime.InvalidSpecException
import spock.lang.FailsWith
import spock.lang.Specification

class MockStubSpyTest extends Specification 

  static class Publisher 
    List<Subscriber> subscribers = new ArrayList<>()

    void addSubscriber(Subscriber subscriber) 
      subscribers.add(subscriber)
    

    void send(String message) 
      for (Subscriber subscriber : subscribers)
        subscriber.receive(message);
    
  

  static interface Subscriber 
    String receive(String message)
  

  static class MySubscriber implements Subscriber 
    @Override
    String receive(String message) 
      if (message ==~ /[A-Za-z ]+/)
        return "ok"
      return "uh-oh"
    
  

  Subscriber realSubscriber1 = new MySubscriber()
  Subscriber realSubscriber2 = new MySubscriber()
  Publisher publisher = new Publisher(subscribers: [realSubscriber1, realSubscriber2])

  def "Real objects can be tested normally"() 
    expect:
    realSubscriber1.receive("Hello subscribers") == "ok"
    realSubscriber1.receive("Anyone there?") == "uh-oh"
  

  @FailsWith(TooFewInvocationsError)
  def "Real objects cannot have interactions"() 
    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then:
    2 * realSubscriber1.receive(_)
  

  def "Stubs can simulate behaviour"() 
    given:
    def stubSubscriber = Stub(Subscriber) 
      receive(_) >>> ["hey", "ho"]
    

    expect:
    stubSubscriber.receive("Hello subscribers") == "hey"
    stubSubscriber.receive("Anyone there?") == "ho"
    stubSubscriber.receive("What else?") == "ho"
  

  @FailsWith(InvalidSpecException)
  def "Stubs cannot have interactions"() 
    given: "stubbed subscriber registered with publisher"
    def stubSubscriber = Stub(Subscriber) 
      receive(_) >> "hey"
    
    publisher.addSubscriber(stubSubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then:
    2 * stubSubscriber.receive(_)
  

  def "Mocks can simulate behaviour and have interactions"() 
    given:
    def mockSubscriber = Mock(Subscriber) 
      3 * receive(_) >>> ["hey", "ho"]
    
    publisher.addSubscriber(mockSubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then: "check interactions"
    1 * mockSubscriber.receive("Hello subscribers")
    1 * mockSubscriber.receive("Anyone there?")

    and: "check behaviour exactly 3 times"
    mockSubscriber.receive("foo") == "hey"
    mockSubscriber.receive("bar") == "ho"
    mockSubscriber.receive("zot") == "ho"
  

  def "Spies can have interactions"() 
    given:
    def spySubscriber = Spy(MySubscriber)
    publisher.addSubscriber(spySubscriber)

    when:
    publisher.send("Hello subscribers")
    publisher.send("Anyone there?")

    then: "check interactions"
    1 * spySubscriber.receive("Hello subscribers")
    1 * spySubscriber.receive("Anyone there?")

    and: "check behaviour for real object (a spy is not a mock!)"
    spySubscriber.receive("Hello subscribers") == "ok"
    spySubscriber.receive("Anyone there?") == "uh-oh"
  

  def "Spies can modify behaviour and have interactions"() 
    given:
    def spyPublisher = Spy(Publisher) 
      send(_) >>  String message -> callRealMethodWithArgs("#" + message) 
    
    def mockSubscriber = Mock(MySubscriber)
    spyPublisher.addSubscriber(mockSubscriber)

    when:
    spyPublisher.send("Hello subscribers")
    spyPublisher.send("Anyone there?")

    then: "check interactions"
    1 * mockSubscriber.receive("#Hello subscribers")
    1 * mockSubscriber.receive("#Anyone there?")
  

【讨论】:

mock 和 stub 之间的区别在这里并不清楚。使用模拟,人们想要验证行为(是否以及该方法将被调用多少次)。使用存根,只验证状态(例如测试后集合的大小)。仅供参考:模拟也可以提供准备好的结果。 感谢@mikhail 和chipiik 的反馈。我已经更新了我的答案,希望能改进和澄清我最初写的一些东西。免责声明:在我最初的回答中,我确实说过我过度简化并略微伪造了一些与 Spock 相关的事实。我希望人们了解存根、模拟和间谍之间的基本区别。 @chipiik,作为对您评论的回复,还有一件事:我多年来一直在指导开发团队,并看到他们将 Spock 或其他 JUnit 与其他模拟框架一起使用。在大多数情况下,使用模拟时,他们这样做不是为了验证行为(即计数方法调用),而是为了将被测对象与其环境隔离开来。交互计数 IMO 只是一个附加的好东西,应该慎重谨慎地使用,因为当他们测试组件的接线而不是其实际行为时,此类测试往往会中断。 简短但仍然很有帮助的答案【参考方案2】:

问题是在 Spock 框架的上下文中,我认为当前的答案没有考虑到这一点。

基于Spock docs(自定义示例,添加我自己的措辞):

存根: 用于使协作者以某种方式响应方法调用。存根方法时,您不关心该方法是否会被调用以及调用多少次;您只希望它在被调用时返回一些值或执行一些副作用。

subscriber.receive(_) >> "ok" // subscriber is a Stub()

Mock: 用于描述规范中的对象与其合作者之间的交互。

def "should send message to subscriber"() 
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("hello") // subscriber is a Mock()

Mock 可以充当 Mock 和 Stub:

1 * subscriber.receive("message1") >> "ok" // subscriber is a Mock()

间谍: 总是基于一个真实的对象,用原始的方法来做真实的事情。可以像存根一样使用来更改选择方法的返回值。可以像 Mock 一样用来描述交互。

def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])

def "should send message to subscriber"() 
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("message1") >> "ok" // subscriber is a Spy(), used as a Mock an Stub


def "should send message to subscriber (actually handle 'receive')"() 
    when:
        publisher.send("hello")

    then:
        1 * subscriber.receive("message1") // subscriber is a Spy(), used as a Mock, uses real 'receive' function


总结:

Stub() 是一个 Stub。 Mock() 是 Stub 和 Mock。 Spy() 是 Stub、Mock 和 Spy。

如果 Stub() 足够,请避免使用 Mock()。

如果可以,请避免使用 Spy(),否则可能会产生异味并暗示测试不正确或被测对象的设计不正确。

【讨论】:

只是补充一点:您希望尽量减少使用模拟的另一个原因是模拟与断言非常相似,因为您在模拟上检查可能无法通过测试的内容,然后您总是希望尽量减少您在测试中所做的检查,以保持测试的重点和简单。所以理想情况下,每个测试应该只有一个模拟。 "Spy() 是一个 Stub、Mock 和 Spy。"诗乃间谍不是这样吗? 我只是快速浏览了一下诗乃间谍,他们看起来不像 Mocks 或 Stubs。请注意,这个问题/答案是在 Spock 的上下文中,它是 Groovy,而不是 JS。 这应该是正确的答案,因为这仅限于 Spock 上下文。此外,说存根是花哨的模拟可能会产生误导,因为模拟具有存根没有的额外功能(检查调用计数)(模拟> 比存根更好)。同样,按照 Spock 进行模拟和存根。【参考方案3】:

简单来说:

Mock:你模拟一个类型并在运行中创建一个对象。此模拟对象中的方法返回返回类型的默认值。

存根:您创建一个存根类,其中根据您的要求使用定义重新定义方法。例如:在真实对象方法中,您调用外部 api 并返回用户名和 id。在存根对象方法中,您返回一些虚拟名称。

间谍:你创造了一个真实的对象,然后你监视它。现在您可以模拟一些方法并选择不这样做。

一个用法区别是您不能模拟方法级别的对象。而您可以在方法中创建一个默认对象,然后对其进行监视以获取被监视对象中方法的所需行为。

【讨论】:

【参考方案4】:

存根实际上只是为了方便单元测试,它们不是测试的一部分。模拟,是测试的一部分,验证的一部分,通过/失败的一部分。

因此,假设您有一个将对象作为参数的方法。你永远不会在测试中做任何改变这个参数的事情。您只需从中读取一个值。这是一个存根。

如果您更改任何内容,或者需要验证与对象的某种交互,那么它就是一个模拟。

【讨论】:

我认为该对象被称为dummy 而不是stub

以上是关于Spock测试框架中Mock / Stub / Spy之间的区别的主要内容,如果未能解决你的问题,请参考以下文章

Spock单元测试框架实战指南二-mock第三方依赖

Spock框架Mock对象方法经验总结#yyds干货盘点#

单元测试之Stub和Mock

Mock 或 Stub 有什么区别?

Golang Stub初体验

单元测试