伪造、模拟和存根之间有什么区别?

971
我知道如何使用这些术语,但我想知道单元测试中是否有关于"伪造"、"模拟"和"桩"的被广泛接受的定义?你是如何为你的测试定义这些术语的?描述一下你可能在哪些情况下使用它们。
以下是我对它们的理解:
"伪造":一个实现了接口但只包含固定数据和没有逻辑的类。根据实现返回"好"或"坏"的数据。
"模拟":一个实现了接口的类,允许动态设置特定方法返回的值/抛出的异常,并提供检查特定方法是否调用/未调用的能力。
"桩":类似于模拟类,但它不提供验证方法是否调用/未调用的能力。
生成方式如下:
- "模拟"和"桩"可以手动生成,也可以由模拟框架生成。 - "伪造类"需要手动生成。
使用方式如下:
  • 模拟对象主要用于验证我的类与依赖类之间的交互。
  • 存根一旦我验证了交互并测试了代码中的替代路径。
  • 虚拟类主要用于抽象出数据依赖,或者当模拟对象/存根设置每次都太繁琐时。

22
你在你的“问题”中已经说得很清楚了 :) 我认为这些都是这些术语相当被广泛接受的定义。 - Eran Galperin
6
维基百科关于Fake的定义与此不同,声称Fake“用作更简单的实现,例如在测试中使用内存数据库代替真正的数据库访问”。请参见 https://en.wikipedia.org/wiki/Test_double。 - zumalifeguard
7
我从以下资源中学到了很多,其中Robert C. Martin(叔叔鲍勃)的解释非常好:《The Little Mocker on The Clean Code Blog》。它解释了哑元、测试替身、存根、间谍、(真正的)模拟和伪物之间的区别和微妙之处。它还提到了Martin Fowler并简要解释了一些软件测试历史。 - Erik
1
请点击以下链接阅读有关测试替身的文章:https://testing.googleblog.com/2013/07/testing-on-toilet-know-your-test-doubles.html(一篇简短的一页摘要)。 - ShreevatsaR
1
这是我的解释:测试替身:伪对象、存根和模拟(带有示例的博客文章) - michal-lipski
“模拟和存根单元测试”没有任何“被接受的定义”,因为“单元”本身也没有定义。 - Konstantin Ivanov
16个回答

698
您可以获取以下信息:

来自Martin Fowler关于Mock和Stub的文章

Fake对象实际上具有工作实现,但通常采用某些快捷方式,使它们不适合生产。

Stubs在测试期间为调用提供预先设置的答案,通常对外部任何内容都不做出响应,除非是测试程序中编程的内容。 Stub也可以记录有关调用的信息,例如记住发送的消息的邮件网关存根,或者仅记住发送了多少个消息。

Mocks是我们在这里谈论的:预编程的对象,其期望形成对它们接收到的调用的规范。

来自xunitpattern

:

Fake:我们获取或构建一个非常轻量级的功能与SUT依赖的组件提供的相同,然后指示SUT使用它而不是真正的。

Stub:此实现被配置为响应来自SUT的调用,使用将在SUT内部执行未测试代码的值(或异常)。使用测试存根的关键指标是具有由于无法控制SUT的间接输入而导致的Untested Code(请参见第X页的Production Bugs)

Mock Object:实现与SUT (System Under Test) 依赖的对象相同的接口。当我们需要进行行为验证时,可以使用模拟对象作为观察点,以避免由于无法观察到对SUT调用方法的副作用而导致的未经测试的要求(请参见第X页的Production Bugs)。

个人理解

我尝试使用 Mock 和 Stub 来简化。当被测试类需要返回一个值时,我使用 Mock。而当需要模拟接口或抽象类进行测试时,我则使用 Stub。事实上,你可以称之为任何名称,它们都是不用于生产的类,并且用作测试的实用工具类。


23
在xUnitPattern引用中,我认为Stub和Fake的定义与Martin Fowler的引用相反。此外,Martin Fowler对Stub和Fake的定义与tvanfosson原始问题中的定义相反。实际上,这两个术语是否有普遍接受的定义取决于你与谁交流。 - Simon Elms
4
谢谢您的夸奖!我会尝试通过使用“模拟”和“桩”的方式来简化。这是一个好主意!+1 - Brad Cupit
5
仅使用 Mock 和 Stub 并不是一个好主意,因为每个测试替身都有其针对的目的和用途。 - Hector Ordonez
2
@MusuNaji:在 MF 的定义中,与 Fake 相关的对话中没有“期望”,除了它有一个接口的实现。另一方面,Mock 将会受到挑战(这个方法被调用了吗?)。 - dbalakirev
2
@Luke 我们正在谈论单元测试,这与集成测试不同。集成测试应该有一个真实的系统支持它。但是,如果您可以在集成测试中使用模拟对象,则可以使用伪造对象或存根(取决于您的定义)。如果必须使用模拟对象,则您可能正在测试模拟对象,而不是集成。然而,模拟对象确实非常有用的一种情况是当您有一个编写不良的 API 时,无法轻松地进行测试。在这些情况下,模拟对象可以拯救生命。当然,这只是我的个人意见。 - Brill Pappin
显示剩余3条评论

306

存根 - 一个为方法调用提供预定义答案的对象。

模拟对象 - 一个对象,你可以在其上设置期望。

伪对象 - 一个具有有限能力(用于测试目的)的对象,例如伪造的 web 服务。

测试替身是指存根,模拟对象和伪对象的通用术语。但非正式地,人们经常简单地称它们为模拟对象。


8
在这个语境中,“canned answer”是什么意思?谁能解释并给我定义一下?“canned answer”在这里指的是预设的标准回答,通常不经过个性化定制。 - MasterMastic
24
一个明确的值,而不是计算出来的值。 - Mike
终于!一些我可以理解的定义!基于这些定义,googletest (gtest) / googlemock (gmock) 允许模拟对象也成为存根,因为您可以在模拟方法上创建 EXPECT_CALL(),强制根据某些输入产生特定输出,通过使用附加到 EXPECT_CALL().WillOnce(Invoke(my_func_or_lambda_func))(或 .WillRepeatedly())类型语法。关于如何使用 Invoke() 的一些示例可以在此处查看:https://dev59.com/AlcP5IYBdhLWcg3wqLzA#60905880。 - Gabriel Staples
我不喜欢这个术语。这个“在模拟对象上设置期望”的做法要么是错误的抽象,要么太过专业化。你可以直接创建一个模拟对象,对其进行某些函数操作,并将结果与某些已知结果进行比较。而“设置期望”从未以明确的方式完成。实际上,在许多框架中,期望并不被视为模拟的一部分。 - IceFire
@IceFire - 或许更清晰的表述是“一个你可以验证交互的对象”。我的措辞/经验来自于使用 https://site.mockito.org/(10年前)。 - Mike
显示剩余2条评论

127
我很惊讶这个问题已经存在了这么长时间,但是还没有人根据Roy Osherove的《单元测试的艺术》提供答案。
在“3.1引入存根”中,定义存根为:
一个存根是系统中现有依赖项(或协作者)的可控替代品。通过使用存根,您可以在不直接处理依赖关系的情况下测试代码。
并且将存根和模拟之间的区别定义为:
关于模拟和存根的主要区别是,模拟与存根非常相似,但您对模拟对象进行断言,而不对存根进行断言。
Fake只是用于存根和模拟的名称。例如,当您不关心存根和模拟之间的区别时。
Osherove所区分的存根和模拟的方式意味着,用作测试的任何类都可以是存根或模拟。它对于特定测试来说是什么取决于您如何编写测试中的检查。
  • 当你的测试检查被测试类中的值或者任何其他地方的值,但不包括伪造对象时,伪造对象被用作存根。它只提供被测试类使用的值,直接通过调用返回的值或者间接地通过调用引起副作用 (在某个状态下)。
  • 当你的测试检查伪造对象的值时,它被用作模拟对象。

一个将FakeX类用作存根的测试示例:

const pleaseReturn5 = 5;
var fake = new FakeX(pleaseReturn5);
var cut = new ClassUnderTest(fake);

cut.SquareIt;

Assert.AreEqual(25, cut.SomeProperty);

"

fake实例被用作存根,因为Assert根本不使用fake

测试类X被用作模拟的示例:

"
const pleaseReturn5 = 5;
var fake = new FakeX(pleaseReturn5);
var cut = new ClassUnderTest(fake);

cut.SquareIt;

Assert.AreEqual(25, fake.SomeProperty);

在这种情况下,Assert检查了fake上的一个值,使得该fake成为了一个mock。
当然,这些示例非常虚构,但我认为这种区分非常有价值。它让你意识到你如何测试你的东西以及你的测试依赖关系在哪里。
我同意Osherove的观点:

从纯可维护性的角度来看,使用mock在我的测试中比不使用mock更麻烦。这是我的经验,但我总是学到新东西。

尽量避免针对fake进行断言,因为这会使你的测试高度依赖于一个根本不是被测试对象的类的实现。这意味着,如果ClassUsedAsMock的实现发生了改变,那么ActualClassUnderTest的测试可能会开始出现问题。这对我来说很糟糕。最好只有在ActualClassUnderTest发生改变时才会导致ActualClassUnderTest的测试失败。
我意识到针对虚假情况写出断言是一种常见的做法,特别是当你是一位模拟测试驱动开发者时。我想我坚定地站在经典派阵营中(参见Martin Fowler的“Mocks aren't Stubs”),并像Osherove一样尽可能避免交互测试(只能通过针对虚假情况进行断言来完成)。
如果您想了解有关为什么应该避免使用此处定义的模拟对象的有趣阅读,请搜索“fowler mockist classicist”。您会发现大量不同的观点。

112

如最受欢迎的答案所述,马丁·福勒在《Mocks Aren't Stubs》一文中讨论了这些区别,特别是子标题「Mocks and Stubs的区别」,请务必阅读该文章。

我认为,与其关注这些概念的不同之处,还不如聚焦于为什么它们是不同的概念,这更能启发人们。每个概念都有其不同的目的。

Fakes

伪装是一种行为“自然”但不是“真实”的实现。这些是模糊概念,因此不同的人对何为伪装有不同的理解。

一个伪装的例子是内存数据库(例如使用sqlite和:memory:存储)。您不会在生产中使用它(因为数据未持续保存),但它完全可以作为测试环境中使用的数据库。它也比“真实”数据库更轻量级。

再例如,也许您在生产中使用某种对象存储(例如Amazon S3),但在测试中,您可以将对象简单地保存到磁盘文件中;那么“保存到磁盘”的实现就是一个伪装。(或者您甚至可以通过使用内存文件系统来模拟“保存到磁盘”操作。)

第三个例子,想象一下提供缓存API的对象; 实现正确接口但根本不进行任何缓存且总是返回缓存未命中的对象就是某种类型的伪装。

伪装的目的不是影响正在测试的系统的行为,而是简化测试的实现(通过消除不必要或过重的依赖关系)。

Stubs

存根是一种行为“不自然”的实现。它是预先配置的(通常由测试设置)以对特定输入作出特定输出。

存根的目的是将正在测试的系统置于特定状态中。例如,如果您编写与REST API交互的代码的测试,则可以使用始终返回固定响应或对API请求以特定错误响应的API来存根化REST API。这样您就可以编写测试,对系统如何响应这些状态进行断言;例如,测试用户在API返回404错误时获得的响应。

存根通常被实现为仅响应您已告知其响应的确切交互。但将某些东西定义为存根的关键特征是它的目的:存根是关于设置测试案例的一切。

Mocks

一个模拟对象(mock)类似于存根(stub),但是加入了验证(verification)模拟对象的目的是对测试系统与其依赖之间的交互方式进行断言。 例如,如果您正在为将文件上传到网站的系统编写测试,那么您可以构建一个接受文件并且您可以用来断言上传的文件是否正确的模拟对象(mock)。或者,在较小的范围内使用对象的Mock来验证被测试系统调用的模拟对象特定方法是很常见的。 模拟对象(mock)交互测试相关联,这是一种特定的测试方法。喜欢测试系统状态而不是系统交互的人将尽可能少地使用模拟对象(mock)

测试替身

伪造对象(Fake)、存根(Stub)和模拟对象(Mock) 都属于测试替身类别。测试替身是在测试中代替其他对象或系统的任何对象或系统。大多数自动化软件测试都涉及某种形式的测试替身。其他测试替身包括虚拟值(dummy values)间谍(spy)I/O黑洞

9
我已阅读所有答案,我认为这是迄今为止最好的澄清说明。 - Basil Musa
2
多年后再次阅读,仍然是最好的答案之一。 - C S
什么是“交互测试”,就像在“Mocks are tied to interaction testing [...]”中所说的那样?它与“集成测试”有何不同? - alelom
除了测试双倍体这个术语可以包含 Fakes、Mocks 和 Stubs,Fake 本身有时也被认为是一个泛指,包括 Mocks 和 Stubs。 - Chris Halcrow

31

你所断言的对象被称为模拟对象。

其他只是帮助测试运行的存根


4
其他答案的细节很好,而且非常棒。但是这个答案让区别变得如此清晰易懂,难以不点赞。干得好! - Mario Garcia

18
所有这些都被称为测试替身,用于注入您的测试用例需要的依赖项。

Fake

桩: 它已经有一个预定义的行为来设置您的期望。 例如,桩仅返回API响应的成功情况 Stub

模拟是一种更智能的桩。您可以通过它验证测试是否通过。 因此,您可以制作一个模拟,该模拟根据条件在测试用例中进行更改,返回成功或失败的情况。 Mock

Dummy

Spy


很有趣能在Swift中看到这些,与我目前使用的JavaScript/Jest工具相比。 - blwinters

16

测试替身类型

单元测试 - 是一种测试方法,其中单元(类、方法)处于控制之下。

测试替身 - 不是面向对象编程世界中的主要对象。它是一个实现,被临时创建用于测试、检查或开发过程中。它们被创建用于关闭被测试单元(方法、类等)的依赖关系

测试替身类型:

  • 虚拟对象 是接口(协议)的真正实现或使用继承或其他方法创建的扩展,可以用于创建依赖项。通常由开发人员创建作为替换某些依赖项的最简单解决方案。

  • 存根对象 是一个裸对象(0、nil和没有逻辑的方法),具有预定义的额外状态,用于定义返回值。通常由框架创建。

class StubA: A {
    override func foo() -> String {
        return "My Stub"
    }
}
  • Mock对象Stub对象非常相似,但在程序执行期间会更改额外状态以检查是否发生了某些事情(方法是否被调用、参数、何时、多少次等)。
class MockA: A {
    var isFooCalled = false
    override func foo() -> String {
        isFooCalled = true
        return "My Mock"
    }
}
  • spy object 是一个具有“部分模拟”的真实对象。这意味着您使用的是一个非 double 对象,除了模拟的行为之外。

  • dummy object 是必要的对象,用于运行测试,但不调用此对象的任何变量或方法。

存根 vs 模拟

Martin Fowler 说过

区别在于存根使用状态验证,而模拟使用行为验证。

[Mockito mock vs spy]


14

为了说明存根(stubs)和模拟对象(mocks)的用法,我想基于Roy Osherove的《单元测试的艺术》包含一个例子。

假设我们有一个LogAnalyzer应用,其唯一功能是打印日志。它不仅需要与Web服务交互,而且如果Web服务抛出错误,LogAnalyzer还必须将错误记录到不同的外部依赖项,通过电子邮件将其发送给Web服务管理员。

这里是我们想在LogAnalyzer内测试的逻辑:

if(fileName.Length<8)
{
 try
  {
    service.LogError("Filename too short:" + fileName);
  }
 catch (Exception e)
  {
    email.SendEmail("a","subject",e.Message);
  }
}

当Web服务抛出异常时,如何测试LogAnalyzer正确调用电子邮件服务?以下是我们面临的问题:

  • 我们如何替换Web服务?

  • 我们如何模拟Web服务中的异常,以便测试调用电子邮件服务?

  • 我们如何知道电子邮件服务是否被正确调用或根本没有被调用?

我们可以通过使用Web服务的存根(stub)来解决前两个问题。为了解决第三个问题,我们可以使用电子邮件服务的模拟对象(mock object)

虚拟对象(fake)是一个通用术语,可用于描述存根或模拟对象。在我们的测试中,将有两个虚拟对象。一个是电子邮件服务模拟对象,我们将使用它来验证发送到电子邮件服务的正确参数。另一个是存根,我们将使用它来模拟从Web服务抛出的异常。它是一个存根,因为我们不会使用Web服务的虚拟对象来验证测试结果,只是确保测试运行正确。电子邮件服务是一个模拟对象,因为我们将针对它进行断言以证明它已被正确调用。

[TestFixture]
public class LogAnalyzer2Tests
{
[Test]
 public void Analyze_WebServiceThrows_SendsEmail()
 {
   StubService stubService = new StubService();
   stubService.ToThrow= new Exception("fake exception");
   MockEmailService mockEmail = new MockEmailService();

   LogAnalyzer2 log = new LogAnalyzer2();
   log.Service = stubService
   log.Email=mockEmail;
   string tooShortFileName="abc.ext";
   log.Analyze(tooShortFileName);

   Assert.AreEqual("a",mockEmail.To); //MOCKING USED
   Assert.AreEqual("fake exception",mockEmail.Body); //MOCKING USED
   Assert.AreEqual("subject",mockEmail.Subject);
 }
}

11

如果您熟悉“安排-操作-断言” (Arrange-Act-Assert) 测试模式,那么解释存根 (stub) 和模拟 (mock) 之间的差异可能会对您有所帮助。存根属于安排 (arrange) 部分,因为它们用于安排输入状态;而模拟属于断言 (assert) 部分,因为它们用于对结果进行断言。

虚拟对象 (Dummy) 并不做任何事情。它们只是用于填充参数列表,以避免未定义或空指针错误。它们也存在于静态类型语言中满足类型检查器的要求,以便您可以编译和运行代码。


8
存根(Stub)、伪对象(Fakes)和模拟对象(Mocks)在不同的来源中有不同的含义。我建议您介绍内部术语并就其含义达成共识。
我认为区分两种方法很重要: - 行为验证(意味着行为替换) - 终态验证(意味着行为仿真)
以发送错误邮件为例。在进行行为验证时,您需要检查IEmailSenderSend方法是否执行一次。您需要仿真此方法的返回结果,并返回已发送消息的ID。因此,您可以说:“我期望调用Send方法,并且我将为任何调用返回虚拟(或随机)ID。”这是行为验证:emailSender.Expect(es=>es.Send(anyThing)).Return((subject,body) => "dummyId") 在进行状态验证时,您需要创建实现IEmailSender接口的TestEmailSender。然后,通过将输入保存到一些数据结构中,例如一组对象数组SentEmails,并在以后的状态验证中测试,来实现Send方法。在进行测试时,您需要检查SentEmails是否包含预期的电子邮件。这是状态验证:Assert.AreEqual(1, emailSender.SentEmails.Count) 从我的阅读中了解到,行为验证通常称为模拟对象。 而状态验证通常称为存根伪对象

1
非常详细和清晰的定义。 - Shyam

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