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


101

我不了解Spock测试中的Mock,Stub和Spy之间的区别,而我一直在网上查看的教程也没有详细解释。

Answers:


94

注意:在接下来的几段中,我将过分简化,甚至稍加捏造。有关更多详细信息,请参阅Martin Fowler的网站

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

存根也是一个虚拟类,为某些被测请求提供了一些更具体的,准备好的或预先记录的,重放的结果。您可以说存根是一种幻想。在Spock中,您将经常阅读有关存根方法的信息。

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


更新2017-02-06:实际上,用户mikhail的答案比我上面的原始答案更特定于Spock。因此,在Spock的范围内,他所描述的是正确的,但这并不能伪造我的一般答案:

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

现在,这里是一个可执行的示例测试,演示了什么是可能的,什么不是。比mikhail的摘要更具启发性。非常感谢他激励我改善自己的答案!:-)

package de.scrum_master.stackoverflow

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

模拟和存根之间的区别在这里尚不清楚。使用模拟,人们想验证行为(如果要调用该方法以及调用多少次)。使用存根,仅验证状态(例如,测试后收集的大小)。仅供参考:模拟也可以提供准备好的结果。
chipiik

感谢@mikhail和Chipiik的反馈。我已经更新了答案,希望可以改善和澄清我最初写的一些东西。免责声明:在我最初的回答中,我确实说过我简化了一些与Spock有关的事实,并稍稍捏造了一些事实。我希望人们了解短截,嘲笑和间谍之间的基本区别。
kriegaex

@chipiik,作为对您的评论的答复,还有一件事:我多年来一直在指导开发团队,并看到他们将Spock或其他JUnit与其他模拟框架一起使用。在大多数情况下,使用模拟时,他们不是为了验证行为(即,计数方法调用)而是为了将被测对象与其环境隔离开来。交互计数IMO只是一个附加功能,应慎重且谨慎地使用,因为当这些测试对组件的布线的测试超出其实际行为时,这种测试就有可能中断。
kriegaex

它的简短但仍然很有帮助的答案
Chaklader Asfak Arefe

55

问题是在Spock框架的背景下进行的,我不认为当前的答案会考虑到这一点。

基于Spock文档(示例已定制,并添加了我自己的措辞):

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

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

模拟: 用于描述规范下的对象与其协作者之间的交互。

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

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

模拟可以充当模拟和存根:

1 * subscriber.receive("message1") >> "ok" // subscriber is a 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()是存根和模拟。
  • Spy()是Stub,Mock和Spy。

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

如果可以的话,避免使用Spy(),否则可能会闻到气味并提示测试不正确或被测试对象的设计不正确。


1
只是要补充:要尽量减少使用模拟的另一个原因是,模拟与断言非常相似,因为您在模拟中检查可能无法通过测试的内容,并且始终希望将检查数量最小化您可以在测试中进行操作,以保持测试的重点和简单性。因此,理想情况下,每个测试只能有一个模拟。
Sammi

1
“ Spy()是存根,模拟和间谍。” 这对西农间谍不是真的吗?
K-SO的毒性在增加。

2
我只是快速看了一下Sinon间谍,它们看起来不像假装或存根。请注意,此问题/答案是在Spock的上下文中进行的,它是Groovy而不是JS。
mikhail

这应该是正确的答案,因为这仅限于Spock上下文。另外,说存根是一个花哨的模拟可能会误导,因为该模拟具有存根不具备的其他功能(检查调用计数)(模拟>比存根更高级)。同样,按照Spock进行模拟和存根。
CGK

13

简单来说:

模拟:您模拟一种类型,并即时创建一个对象。该模拟对象中的方法返回返回类型的默认值。

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

监视:创建一个真实的对象,然后监视它。现在,您可以模拟某些方法,而对于某些方法则选择不这样做。

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


0

存根实际上仅是为了促进单元测试,而不是测试的一部分。模拟是测试的一部分,是验证的一部分,是通过/失败的一部分。

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.