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

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

67746 次浏览

注意: 在接下来的段落中,我将过分简化甚至可能稍微造假。更详细的信息见 Martin Fowler 的网站

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

存根也是一个虚拟类,为测试中的特定请求提供一些更具体的、准备好的或预先录制的重播结果。你可以说票根是一个花哨的玩笑。在 Spock 中你会经常读到存根方法。

间谍是一种真实对象和存根的混合体,也就是说,它基本上是真实对象,一些(不是全部)方法被存根方法遮蔽。非存根方法只是路由到原始对象。这样,您可以为“廉价”或琐碎的方法提供原始行为,为“昂贵”或复杂的方法提供假行为。


Update 2017-02-06: Actually user mikhail's answer is more specific to Spock than my original one above. So within the scope of Spock, what he describes is correct, but that does not falsify my general answer:

  • 存根与模拟特定行为有关。在 Spock 中,这是存根所能做的一切,所以这是最简单的事情。
  • 模拟涉及到代表一个(可能代价高昂的)实际对象,为所有方法调用提供不可操作的答案。在这方面,mock 比存根更简单。但是在 Spock 中,mock 也可以存根化方法结果,也就是说,既可以是 mock 也可以是 stub。此外,在 Spock 中,我们可以计算在测试期间调用具有某些参数的特定 mock 方法的频率。
  • 间谍总是包装实际对象,默认情况下将所有方法调用路由到原始对象,也传递原始结果。方法调用计数也适用于间谍。在 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?")
}
}

试试 Groovy Web 控制台

简而言之:

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

存根: 您创建了一个存根类,在这个类中方法根据您的需求通过定义重新定义。例: 在实际的对象方法中,你调用外部的 api 并返回用户名和 id。在存根对象方法中,返回一些虚拟名称。

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

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

这个问题是在斯波克框架的背景下提出的,我认为目前的答案没有考虑到这一点。

基于 史波克博士(例子定制,我自己的措辞添加) :

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

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

Mock: < em > 用于描述规范下的对象与其协作者之间的交互。

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: < em > 总是基于一个实际的对象,使用原始的方法来做实际的事情。可以像 Stub 一样用于更改选择方法的返回值。可以像 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。
  • 间谍()是一个存根,模拟和间谍。

如果 Stub ()已经足够,就避免使用 Mock ()。

尽可能避免使用 Spy () ,因为必须这样做可能会产生气味,并暗示被测对象的测试或设计不正确。

Stubs are really only to facilitate the unit test, they are not part of the test. Mocks, are part of the test, part of the verification, part of the pass / fail.

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

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