模拟和桩之间有什么区别?

1359

175
因为没有区别。尽管该文章备受社区喜爱,但它却不必要地给本来易于理解的词汇添加额外的含义,并使事情变得不必要地复杂,从而让一切变得混乱。Mock只是一个模拟,运行虚假业务逻辑而不是真实的业务逻辑。最后检查其行为是你的选择,但它仍然是一个mock。或者你想用其他任何称呼,但请把它统一起来,不要纠缠于细节,保持简单,这样人们才能容易地理解你的概念 - 此前提到的文章在这方面失败了。 - wst
28
文献中关于模拟对象、伪对象和桩对象的分类标准极不一致。该句话有多处引用,是我最喜欢的维基百科语录之一。 - JD.
29
Martin Fowler的那篇文章对于初学者来说真的很难理解。 - lmiguelvargasf
1
可能是What's the difference between faking, mocking, and stubbing?的重复问题。 - tvanfosson
显示剩余2条评论
42个回答

1147

前言

有几种并非真实的对象定义,它们的通用术语是测试替身。这个术语包括:虚假对象伪造对象存根模拟对象

参考

根据Martin Fowler的文章

  • 虚拟对象只是在方法参数列表中占位而从未被实际使用过。
  • 伪造对象实际上具有有效的实现,但通常采取了一些捷径,使它们不适用于生产环境(内存数据库是一个很好的例子)。
  • 存根为测试期间进行的调用提供预设的答案,通常不响应任何程序外部的请求。存根也可以记录有关调用的信息,例如记住发送的消息的电子邮件网关存根,或仅记住它“发送”的消息数量。
  • 模拟对象是我们在这里正在讨论的:预编程的对象,其期望形成对其接收到的调用的规范。

风格

模拟对象 vs 存根 = 行为测试 vs 状态测试

原则

根据每个测试只测试一个事物原则,一个测试中可能会有几个存根,但通常只有一个模拟对象。

生命周期

使用存根的测试生命周期:

  1. 设置 - 准备正在测试的对象及其存根协作者。
  2. 执行 - 测试功能。
  3. 验证状态 - 使用断言检查对象的状态。
  4. 拆卸 - 清理资源。

使用模拟对象的测试生命周期:

  1. 设置数据 - 准备正在测试的对象。
  2. 设置期望 - 准备在主对象中使用的模拟期望。
  3. 练习 - 测试功能。
  4. 验证期望 - 验证模拟中已调用正确的方法。
  5. 验证状态 - 使用asserts检查对象的状态。
  6. 拆卸 - 清理资源。
  7. 概述

    Mocks和Stubs测试都可以回答问题:结果是什么?

    使用Mocks进行测试还关注:结果是如何实现的?


等等,模拟对象也返回预设的答案吗?否则为什么它们会回答问题呢? - AturSams
从你写的内容中,我可以得出Mocks = Stubs + Expectations和Verifications的结论,因为Mocks“在测试期间对调用提供预先准备好的答案,通常不会响应测试程序外部未编程的任何内容”(与Stubs相同)。而Fowler所展示的一个Stubs例子实际上是一个Spy的例子!这意味着Mock是Stub,Spy也是Stub。而Stub只是一个具有多个可用方法的对象。这也解释了为什么Mockito已经弃用了Stub()方法。 - kolobok
我对这个问题和被接受的答案感到困惑的是“期望设置”,这到底是什么意思?通常,在“主代码”中,您会创建您所期望的结果。但听起来你把期望放到了模拟对象里面,这对我来说没有意义。此外,您可以轻松地使用一些输入来测试模拟对象,存储结果,稍后再创建“期望”,然后进行比较。您使用的术语我觉得太抽象和模糊了。 - IceFire
为什么这个答案出现在第二页?它应该出现在被接受的答案之后(如果不是在之前)。我在这里看到的最好的答案,详细、精确、易懂。 - Mahdi Tahsildari
我认为“原则”部分是不正确的。测试替身是用来替换“协作者”或“依赖项”的。在一个测试中可能会有几个这样的替身,这是完全可以的。 只有一个被测系统(SUT),这里没有区别。无论是 Mock 还是 Classical 风格的测试都喜欢一个 SUT。 - eisenpony

927

我认为最大的区别在于,桩是具有预定行为的已经编写好的代码。因此,您将拥有一个实现依赖项(很可能是抽象类或接口)的类,用于测试目的,方法只是用设置响应来进行桩处理。它们不会做任何花哨的事情,而且您已经在测试之外编写了这些桩代码。

模拟对象

模拟对象是您在测试中必须根据您的期望设置的内容。模拟对象并未以预定方式设置,因此您需要在测试中编写代码来完成此项工作。某种程度上说,模拟对象是在运行时确定的,因为设置预期的代码必须在它们执行任何操作之前运行。

模拟对象和桩之间的区别

使用模拟对象编写的测试通常遵循“初始化 -> 设置预期 -> 执行 -> 验证”模式进行测试。而预先编写的桩将遵循“初始化 -> 执行 -> 验证”模式。

模拟对象和桩之间的相似之处

两者的目的都是消除类或函数的所有依赖项的测试,使您的测试更加专注和简单,以证明它们试图证明的内容。


4
一句话可能会让人感到困惑-“mock是在测试中,您必须按照您的期望设置的内容。Mock不是以预定方式设置的,因此您需要在测试中编写代码来完成它。” 这句话的意思是,您需要使用mock对象作为测试的一部分,并且您需要根据预期设置该对象。与实际情况不同,mock对象并不是以预先确定的方式设置的,而是需要您在测试中编写代码来模拟其行为。 - Drew
你对桩件的定义与@RyszardDżegan的答案中“伪造”的定义相匹配,我认为。 - Antoniossss
1
根据其他答案和我的经验,我认为存根在编译时是预先确定的,而模拟是在运行时确定的(通过在测试中设置期望值)。 - Kyriakos Xatzisavvas

543

桩对象是一种简单的伪造对象,其作用是确保测试正常运行。
模拟对象是一种更智能的桩对象。您可以通过它验证测试是否通过。


59
我认为这是最简洁而准确的答案。结论:一个模拟的IS-A存根。https://dev59.com/RnA75IYBdhLWcg3wH1XP#17810004是这个答案的较长版本。 - PoweredByRice
16
我认为模拟对象与桩对象不同。模拟对象用于断言并且不应该返回数据,而桩对象用于返回数据并且不应该进行断言。 - dave1010
6
Mock对象肯定可以返回数据甚至抛出异常。它们应该根据传入的参数做出相应的回应。 - Trenton
3
如果一个对象根据传入的数据返回或抛出异常,那么它是一个“仿造对象”,而不是一个模拟对象。桩测试用于测试您的系统单元如何处理“接收”消息,模拟测试用于测试您的系统单元如何“发送”消息。混淆这两个概念很可能会导致糟糕的面向对象设计。 - dave1010
29
我认为这很棒——存根返回问题的答案。模拟也返回问题的答案(是一个存根),但它还验证了问题是否被提出! - Leif
显示剩余3条评论

321

以下是每个的描述,以及实际世界示例。

  • 虚拟 - 只是为了满足API而创建的虚假值。

示例:如果您正在测试一个类的方法,该方法在构造函数中需要许多强制参数,但这些参数对您的测试没有影响,那么您可以创建虚拟对象来创建类的新实例。

  • 伪造 - 创建一个测试实现的类,该类可能依赖于某些外部基础设施。(最好的做法是您的单元测试实际上不与外部基础设施进行交互。)

示例:为访问数据库创建伪造实现,将其替换为内存中的集合。

  • 存根 - 重写方法以返回硬编码的值,也称为基于状态
例子:你的测试类依赖于一个需要5分钟才能完成的方法Calculate()。与其等待5分钟,你可以用返回硬编码值的存根(stub)替换它的真实实现;只需要花费很少一部分时间。
Mock - 与Stub非常相似,但是基于交互(interaction-based)而不是基于状态(state-based)。这意味着你不期望从Mock中返回某个值,而是假定特定的方法调用顺序被执行。
例子:你正在测试一个用户注册类。在调用Save之后,它应该调用SendConfirmationEmail。
Stubs和Mocks实际上是Mock的子类型,两者都将真实实现与测试实现交换,但是出于不同的、具体的原因。

8
  1. 阅读答案
  2. 阅读 https://blog.cleancoder.com/uncle-bob/2014/05/14/TheLittleMocker.html
  3. 再次阅读答案。
- Soner from The Ottoman Empire
我很少在SO上留下评论,但这个答案显然值得点赞。清晰、简洁且带有示例。同时,感谢@snr分享这篇文章。 - antonimmo
1
“Stubs和Mocks实际上是Mock的子类型” --- 你是指Stubs和Fakes吗? - Martin
这可能会有所帮助:http://xunitpatterns.com/Mocks,%20Fakes,%20Stubs%20and%20Dummies.html - Boris Makhlin
2
"Stubs和Mocks实际上是Fake的子类型。我想你是指“Stubs和Mocks实际上是Fake的子类型。”" - Boris Makhlin
显示剩余2条评论

205
codeschool.com的课程Rails Testing for Zombies中,他们给出以下术语定义:

Stub

用指定结果的代码替换方法。

Mock

带有断言方法是否被调用的存根。

所以,正如Sean Copenhaver在他的答案中描述的那样,区别在于mock设置期望(即对它们是否以及如何被调用进行断言)。

为了补充Dillon的帖子,考虑一下这个问题,你有一个名为“MakeACake”的类,它需要几个库:牛奶、鸡蛋、糖和烤箱。 - aarkerio

179

存根不会失败你的测试,模拟可以。


2
我认为这很好,你知道如果测试在重构后具有相同的行为。 - RodriKing
1
@RodriKing 我有同样的感觉。就像使用 Mock 一样,对于生产代码的任何更改 - 您都需要相应地更改测试代码。这是一种痛苦!使用 Stub 的话,感觉就像您一直在测试行为,因此不需要对测试代码进行微小的更改。 - tucq88

128

阅读上面的所有解释,让我来简化一下:

  • 桩代码(Stub):一个假的代码片段,让测试运行,但你并不关心它会发生什么。替代真正的工作代码。
  • 模拟对象(Mock):一个假的代码片段,你需要验证它在测试中被正确调用。替代真正的工作代码。
  • 间谍对象(Spy):一个假的代码片段,拦截和验证对真正工作代码的某些调用,避免替代所有真正的代码。

13
好的回答。根据您的定义,Mock听起来与Spy非常相似。如果您更新回答以包括更多的测试替身,那将是很好的。 - Rowan Gontier
1
这是一个非常好的答案,没有使图像模糊。 - iaforek
1
@O'Rooney 很棒的回答。如果您能更新答案并提供有关 Stub 的更清晰的解释/摘要,那将非常有帮助。 - urosc
1
谢谢你的建议;我原本也认为它适用于模拟,所以我采纳了你的建议,并做了一些其他的调整。 - O'Rooney
1
Google上“stub vs mock”的第一名。干得好@O'Rooney! - RudyOnRails

95
我认为关于这个问题最简单、最清晰的答案来自罗伊·奥舍洛夫在他的书《单元测试的艺术》(第85页)中所给出的答案:

判断我们正在处理存根(Stub)的最简单的方法是注意到存根永远不可能使测试失败。测试使用的断言(asserts)始终针对被测类。

另一方面,测试将使用模拟对象来验证测试是否失败。[...]

同样,模拟对象就是我们用来查看测试是否失败的对象。

存根和模拟都是假的(fake)。

如果您对存根进行断言,则意味着您将存根用作模拟;如果您仅使用存根运行测试而没有对其进行断言,则表示您将存根用作存根。


6
希望你的答案能够被置顶。这里有 R. Osherove 的解释视频:https://youtu.be/fAb_OnooCsQ?t=1006。 - Michael Ekoka

38

Mock是测试行为的一种方式,确保调用了某些特定的方法。

Stub是一个特定对象的可测试版本。

"苹果式"是什么意思?


26
你是什么意思,苹果的方式? - kubi
8
以苹果的方式而非微软的方式 :) - never_had_a_name

29

如果你把它与调试进行比较:

存根就像确保方法返回正确的值。

Mock就像实际进入方法,并确保返回正确的值之前,内部所有内容都是正确的。


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