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

128

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

4个回答

124

注意:在下面的段落中,我将会过度简化,甚至可能有些虚假。如果需要更详细的信息,请参考Martin Fowler的网站

Mock是一个代替真实类的虚拟类,在每个方法调用时返回诸如null或0之类的东西。如果您需要复杂类的虚拟实例,否则此类将使用外部资源(如网络连接、文件或数据库),或者可能使用其他几十个对象,则可以使用Mock。 Mock的优点是可以将被测试的类与系统的其余部分隔离。

Stub也是一个虚拟类,为某些受测试请求提供一些更具体、已准备或预录制的结果。你可以说Stub是一个华丽的Mock。在Spock中,您经常会看到关于Stub方法的内容。

Spy是一种真实对象和Stub之间的混合体,即它基本上是具有某些(不是全部)方法由Stub方法阴影化的真实对象。未阴影化的方法只是通过原始对象路由。这样,您可以获得"便宜"或微不足道的方法的原始行为和"昂贵"或复杂方法的伪行为。

更新2017-02-06:实际上,用户mikhail的答案更具体,适用于Spock。因此,在Spock的范围内,他所描述的是正确的,但这并不虚假我的一般回答:

  • Stub关注模拟特定行为。在Spock中,这是Stub可以做的全部内容,因此它是最简单的东西之一。
  • Mock关注代替(可能很昂贵的)真实对象,为所有方法调用提供无操作答案。在这方面,Mock比Stub更简单。但在Spock中,Mock也可以存根方法结果,即既是Mock又是Stub。另外,在Spock中,我们可以计算测试期间某些参数具有特定Mock方法调用次数。
  • Spy始终包装一个真实对象,并默认将所有方法调用路由到原始对象,同时传递原始结果。Spy也支持方法调用计数。在Spock中,Spy还可以修改原始对象的行为,操纵方法调用参数和/或结果,或阻止调用原始方法。

现在,以下是可执行的示例测试,演示了什么是可能的,以及什么是不可能的。它比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?")
  }
}

Groovy Web Console中尝试一下。


这里并不清楚mock和stub之间的区别。使用mock时,我们想要验证行为(方法将被调用的次数和方式)。而使用stub时,我们只验证状态(例如测试后集合的大小)。顺便说一下:mocks也可以提供预先准备好的结果。 - chipiik
1
感谢@mikhail和chipiik的反馈。我已经更新了我的答案,希望改进并澄清了我最初写的一些内容。免责声明:在我的原始答案中,我确实说过我正在过度简化和略微歪曲一些与Spock相关的事实。我希望人们能够理解存根、模拟和间谍之间的基本区别。 - kriegaex
@chipiik,回复你的评论,还有一件事:我已经指导开发团队多年,并看到他们使用Spock或其他JUnit与其他模拟框架。在大多数情况下,当使用模拟时,他们并不是为了验证行为(即计算方法调用次数),而是为了将测试主题与其环境隔离开来。交互计数在我看来只是一个额外的好处,应该谨慎和节制地使用,因为这种测试往往会在测试组件的布线而不是它们的实际行为时出现故障。 - kriegaex
它的回答简洁明了,但仍然非常有帮助。 - Arefe

70
The question is related to the Spock framework, and I don't think the current answers consider this.
According to the Spock docs (with customized examples and my own wording added): Stub: Used to make collaborators respond to method calls in a specific way. When stubbing a method, you do not care if or how many times the method will be called; you only want it to return a certain value or perform some side effect when it is called.
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()

Spy: 总是基于一个真实的对象,具有原始方法来完成真正的事情。可以像存根一样使用,改变选择方法的返回值。也可以像模拟一样描述交互。

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()是一个存根。
  • Mock()是一个存根和模拟。
  • Spy()是一个存根、模拟和间谍。

如果只需要Stub(),则避免使用Mock()。

如果可以的话,请避免使用Spy(),必须使用它可能会有问题,并暗示测试不正确或被测试对象设计不正确。


1
只是要补充一点:你想要最小化使用模拟对象的另一个原因是,模拟对象与断言非常相似,因为你在模拟对象上检查可能会导致测试失败的事情,而你总是希望最小化测试中的检查数量,以保持测试的专注和简单。因此,理想情况下每个测试只应该有一个模拟对象。 - Sámal Rasmussen
2
"Spy()是一个存根(Stub)、模拟对象(Mock)和间谍(Spy)。这对于sinon间谍不适用吗?" - basickarl
2
我刚刚快速浏览了Sinon间谍,它们看起来不像Mock或Stub那样行为。请注意,这个问题/答案是在Spock(Groovy)的上下文中,而不是JS。 - mikhail
这应该是正确的答案,因为它被限定在 Spock 上下文中。同时,说存根是一种花哨的模拟可能会误导人,因为模拟具有额外的功能(检查调用计数),而存根没有(模拟 > 比存根更高级)。再次强调,这是 Spock 的模拟和存根。 - CGK

16

简单来说:

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

Stub: 你创建一个存根类,在其中重新定义方法以符合你的要求。例如:在真实对象的方法中,你调用外部API并返回ID对应的用户名。在存根对象的方法中,你返回一些虚拟名称。

Spy: 你创建一个真实的对象,然后进行监视。现在,你可以模拟某些方法并选择不这样做某些方法。

一个使用上的区别是你无法模拟方法级别的对象。但你可以在方法中创建一个默认对象,然后对其进行监视,以获取所监视对象中方法的期望行为。


3

存根(Stubs)只是为了方便单元测试而存在的,它们并不是测试的一部分。模拟对象(Mocks)则是测试的一部分,用于验证、用于通过或失败的一部分。

比如说你有一个方法,它以对象作为参数。在测试中,你从未改变过这个参数,只是从中读取某个值。这就是一个存根。

如果你改变了任何东西或需要验证与该对象的某种交互作用,那么它就是模拟对象。


我认为那个对象应该被称为“dummy”,而不是“stub”。 - Number945

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接