我不明白 Spock 测试中 Mock、Stub 和 Spy 之间的区别,并且我一直在网上查看的教程没有详细解释它们。
4 回答
注意:在接下来的段落中,我将过度简化,甚至可能会稍微伪造。有关更多详细信息,请参阅Martin Fowler 的网站。
mock 是一个替代真实类的虚拟类,每个方法调用都返回 null 或 0 之类的东西。如果你需要一个复杂类的虚拟实例,你可以使用一个模拟实例,否则它会使用外部资源,如网络连接、文件或数据库,或者可能使用许多其他对象。模拟的优点是您可以将被测类与系统的其余部分隔离开来。
存根也是一个虚拟类,为某些被测请求提供一些更具体的、准备好的或预先记录的、重放的结果。你可以说存根是一个花哨的模拟。在 Spock 中,您会经常阅读到 stub 方法。
spy 是真实对象和存根之间的混合体,即它基本上是带有一些(不是全部)方法被存根方法遮蔽的真实对象。非存根方法只是路由到原始对象。这样,您可以对“便宜”或琐碎的方法具有原始行为,而对“昂贵”或复杂的方法具有虚假行为。
2017-02-06 更新:实际上用户 mikhail 的回答比我上面的原始回答更具体到 Spock。所以在 Spock 的范围内,他的描述是正确的,但这并不能证伪我的一般回答:
- 存根与模拟特定行为有关。在 Spock 中,这是 stub 所能做的一切,所以这是最简单的事情。
- 模拟与代表(可能很昂贵的)真实对象有关,为所有方法调用提供无操作答案。在这方面,模拟比存根更简单。但是在Spock 中,mock 也可以是stub 方法的结果,即既是mock 又是stub。此外,在 Spock 中,我们可以计算在测试期间调用具有某些参数的特定模拟方法的频率。
- 间谍总是包装一个真实的对象,默认情况下将所有方法调用路由到原始对象,也传递原始结果。方法调用计数也适用于间谍。在 Spock 中,间谍还可以修改原始对象的行为、操纵方法调用参数和/或结果或完全阻止调用原始方法。
现在这里是一个可执行的示例测试,演示什么是可能的,什么是不可能的。它比米哈伊尔的片段更有启发性。非常感谢他激励我改进自己的答案!:-)
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?")
}
}
问题是在 Spock 框架的背景下,我不相信当前的答案会考虑到这一点。
基于Spock 文档(自定义示例,添加了我自己的措辞):
Stub: 用于使协作者以某种方式响应方法调用。存根方法时,您不关心该方法是否会被调用以及调用多少次;您只希望它在被调用时返回一些值或执行一些副作用。
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()
}
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(),否则可能会产生气味并暗示测试不正确或被测对象的设计不正确。
简单来说:
模拟:你模拟一个类型并在运行中创建一个对象。此模拟对象中的方法返回返回类型的默认值。
存根:您创建一个存根类,其中根据您的要求使用定义重新定义方法。例如:在真实对象方法中,您调用外部 api 并返回用户名和 id。在存根对象方法中,您返回一些虚拟名称。
间谍:你创造了一个真实的对象,然后你监视它。现在您可以模拟一些方法并选择不这样做。
一种用法差异是您不能模拟方法级别的对象。而您可以在方法中创建一个默认对象,然后对其进行监视以获取被监视对象中方法的所需行为。
存根实际上只是为了方便单元测试,它们不是测试的一部分。模拟,是测试的一部分,验证的一部分,通过/失败的一部分。
因此,假设您有一个将对象作为参数的方法。你永远不会在测试中做任何改变这个参数的事情。您只需从中读取一个值。那是一个存根。
如果您更改任何内容,或者需要验证与对象的某种交互,那么它就是一个模拟。