伪造,嘲笑和存根之间有什么区别?


704

我知道我如何使用这些术语,但是我想知道是否存在用于单元测试的伪造模拟存根的定义?您如何为测试定义这些?描述您可能会使用每种情况的情况。

这是我的用法:

Fake:实现接口但包含固定数据且没有逻辑的类。只需根据实现返回“好”或“坏”数据。

Mock:实现接口的类,并允许动态设置要返回的值/从特定方法抛出的异常,并提供检查是否已调用特定方法的能力。

存根(Stub):类似于模拟类,不同之处在于它不提供验证方法是否已被调用的能力。

模拟和存根可以手动生成,也可以由模拟框架生成。伪类是手工生成的。我主要使用模拟来验证我的类和依赖类之间的交互。一旦验证了交互作用并测试了代码中的替代路径,便会使用存根。我主要使用伪造的类来抽象出数据依赖关系,或者当模拟/存根过于繁琐而无法每次设置时。


6
好吧,您基本上在您的“问题”中说了一切:)我认为这些是这些术语的公认的定义
Eran Galperin

2
Wikipedia对Fake的定义与此不同,它声称Fake“被用作一种更简单的实现,例如,在测试中使用内存数据库而不是进行真正的数据库访问)”请参见en.wikipedia.org/wiki/Test_double
zumalifeguard

2
我从以下资源中学到了很多东西,并得到了罗伯特·C·马丁(Robert C. Martin)(鲍勃叔叔)的出色解释:清洁代码博客上的小嘲笑者。它解释了假人,测试双打,存根,间谍,(真)模拟和假冒之间的区别和精妙之处。它还提到了Martin Fowler,并解释了一些软件测试历史。
Erik

testing.googleblog.com/2013/07/…(简短的一页摘要)。
ShreevatsaR

以下是我的解释:测试双打:假货,存根和嘲弄(带示例的博客文章)
michal-lipski

Answers:


547

您可以获得一些信息:

Martin Fowler的有关模拟和存根

对象实际上具有有效的实现,但是通常采取一些捷径,这使它们不适合生产

存根提供对测试过程中进行的呼叫的固定答复,通常不响应为测试编程的内容之外的任何内容。存根还可以记录有关呼叫的信息,例如电子邮件网关存根,它可以记住“已发送”的消息,或者仅记住“已发送”的消息数量。

嘲笑是我们在这里谈论的内容:对象被预先编程并带有期望,形成了期望接收的呼叫的规范。

xunitpattern

伪造的:我们获取或构建与SUT依赖的组件所提供的功能相同的功能的非常轻量级的实现,并指示SUT使用它而不是真实的。

存根:此实现被配置为使用将在SUT中执行未经测试的代码(请参阅第X页的生产错误)的值(或异常)响应来自SUT的调用。使用测试存根的一个关键指示是由于无法控制SUT的间接输入而导致未测试代码

模拟对象,实现与SUT(被测系统)所依赖的对象相同的接口。当我们需要进行行为验证时,可以将模拟对象用作观察点,以避免因无法观察调用方法对SUT的副作用而导致未经测试的需求(请参阅第X页的生产错误)。

亲自

我尝试通过使用简化:模拟和存根。当它是一个返回设置为测试类的值的对象时,我会使用Mock。我使用Stub模仿要测试的Interface或Abstract类。实际上,您所说的并不重要,它们都是生产中未使用的所有类,并且用作测试的实用程序类。


9
在我看来,与Martin Fowler的报价相比,xUnitPattern报价中Stub和Fake的定义颠倒了。同样,与特凡弗森最初提出的问题相比,马丁·福勒对存根和假的定义也被颠倒了。实际上,这两个术语是否有任何公认的定义,还是仅取决于您在与谁说话?
西蒙·图西

3
为“我试图通过使用:模拟和存根简化” +1。好主意啊!
布拉德·库皮

4
无法看到仅使用Mock and Stub是一个好主意。每个双重测试都有其用途,因此也有其用途。
Hector Ordonez 2014年

1
在MF的定义中,我看不到Fake和Mock之间的区别。
IdontCareAboutReputationPoints 2015年

2
@MusuNaji:在MF的定义中,关于Fake的对话没有“期望”,除了它的接口有实现。另一方面,模拟将受到挑战(此方法是否被调用?)。
dbalakirev

205

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

模拟 -您在其上设定期望的对象。

-功能有限(出于测试目的)的对象,例如伪造的Web服务。

Test Double是存根,模拟和伪造品的总称。但是非正式地,您经常会听到人们简单地称他们为嘲笑。


4
在这种情况下,有人可以向我解释和定义什么是“罐头答案”吗?
2013年

14
显式值,而不是计算得出的值。
Mike

最后!我可以理解一些定义!然后,基于这些定义,googletest(gtest)/ googlemock(gmock)允许模拟对象也成为存根,因为您可以EXPECT_CALL()在模拟方法上创建,从而通过使用.WillOnce(Invoke(my_func_or_lambda_func))(或与.WillRepeatedly())类型基于某些输入强制某些输出附加到的语法EXPECT_CALL()Invoke()在长答案的底部,可以在不同的上下文中看到一些使用示例:stackoverflow.com/a/60905880/4561887
加布里埃尔·斯台普斯

关于Gmock的文档位于Invoke()github.com/google/googletest/blob/master/googlemock/docs/…。无论如何,结论是:Google模拟(gmock)允许人们轻松创建模拟存根,尽管大多数模拟不是存根。
加布里埃尔·斯台普斯

Mocks是Stub的超集,它们仍然可以返回预定义的答案,但也允许开发人员设置期望。IMO的某些库模糊了所有测试假人的界线。
路加福音

94

令我感到惊讶的是,这个问题已经存在了很长时间,而且还没有人根据Roy Osherove的“单元测试的艺术”给出答案。

在“ 3.1介绍存根”中,将存根定义为:

存根是系统中现有依赖项(或协作者)的可控替代。通过使用存根,您可以测试代码而无需直接处理依赖项。

并将存根和模拟之间的区别定义为:

关于模拟与存根之间要记住的主要事情是,模拟就像存根一样,但是您针对模拟对象断言,而您不针对存根进行断言。

虚假只是存根和模拟的名称。例如,当您不关心存根和模拟之间的区别时。

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,使该假冒成为模拟。

现在,这些示例当然是非常人为设计的,但是我认为这种区别非常有价值。它使您知道如何测试自己的东西以及测试的依赖关系在哪里。

我同意Osherove的观点

从纯粹的可维护性角度来看,在我的测试中,使用模拟比不使用模拟会带来更多的麻烦。那是我的经验,但是我一直在学习新的东西。

断言是您真正要避免的事情,因为它使测试高度依赖于完全不是被测类的类的实现。这意味着ActualClassUnderTest因为ClassUsedAsMock更改了实现,所以针对类的测试可能会开始中断。那给我散发出难闻的气味。ActualClassUnderTest最好仅在ActualClassUnderTest更改时中断测试。

我意识到写断言是对伪造的一种惯例,尤其是当您是TDD订户的嘲笑者类型时。我想我与马丁·福勒(Martin Fowler)在古典主义阵营中坚定不移(请参阅马丁·福勒Martin Fowler)的“模仿不是存根”(Ockserove not not Stubs)),并且像Osherove一样,尽可能避免进行交互测试(只能通过断言来进行测试)。

有趣的阅​​读,为什么你应该避免在这里定义的模拟,谷歌为“福勒模拟古典主义者”。您会发现很多意见。


30

正如投票最多的答案所提到的,马丁·福勒(Martin Fowler)在“ 莫克斯不是存根”中讨论了这些区别,特别是副标题“莫克斯与存根之间的区别”。,因此请务必阅读该文章。

而不是着眼于如何这些东西都是不同的,我认为这是更具启发专注于为什么这些是不同的概念。每种存在的目的都不相同。

假货

一个假的是一个执行,其行为“自然”,而不是“真实的”。这些都是模糊的概念,因此不同的人对使事情变得假的有不同的理解。

伪造品的一个例子是内存数据库(例如,在:memory:商店中使用sqlite )。您永远不会将其用于生产(因为数据不会持久保存),但是它完全可以用作测试环境中的数据库。它也比“真实”数据库轻巧得多。

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

第三个示例,想象一个提供缓存API的对象。一个实现正确接口但根本不执行任何缓存但始终返回缓存未命中的对象将是一种伪造。

伪造的目的不是影响被测系统的行为,而是简化实现测试(通过删除不必要的或重量级的依赖项)。

存根

一个存根的行为方式“不自然”的实现。它已预先配置(通常是通过测试设置)以响应具有特定输出的特定输入。

存根的目的是使被测系统进入特定状态。例如,如果您正在为与REST API交互的某些代码编写测试,则可以存根出的REST API与API,它总是返回一个罐头回应,或者回应给特定错误的API请求。这样,您可以编写测试来断言系统对这些状态的反应。例如,如果API返回404错误,则测试用户获得的响应。

通常仅将存根实现为仅响应您已告诉其响应的确切交互。但是,使某事存根的关键特征是它的目的:存根就是建立测试用例。

cks

一个模拟类似于存根,但验证自动加入。一个模拟的目的是为了让你的测试系统如何与依赖互动的断言

例如,如果您正在为将文件上传到网站的系统编写测试,则可以构建一个模拟接受文件文件,并可以使用该文件断言上传的文件是正确的。或者,在较小的规模上,通常使用对象的模拟来验证被测系统是否调用了模拟对象的特定方法。

模拟是与交互测试联系在一起的,交互测试是一种特定的测试方法。喜欢测试系统状态而不是系统交互的人将很少使用模拟程序。

测试双打

伪造品,存根和模拟品均属于测试双打的类别。测试倍数是您在测试中使用的任何对象或系统,而不是其他对象。大多数自动化软件测试都使用某种或多种测试倍数。其他一些测试双精度类型包括伪值间谍和I / O 黑洞


11

为了说明存根和模拟的用法,我还要列举一个基于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服务使用存根来解决前两个问题。为了解决第三个问题,我们可以将模拟对象用于电子邮件服务

假货是一个通用术语,可以用来描述存根或模拟。在我们的测试中,我们将有两个假货。一种是电子邮件服务模拟,我们将使用它来验证是否已将正确的参数发送到电子邮件服务。另一个将是一个存根,我们将使用它来模拟从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);
 }
}

9

您在其中声明的东西称为模拟对象,而其他仅有助于测试运行的东西都是存根


1
而其他答案非常详细,而且非常好。这一点非常明显,容易做出改变,很难不赞成。j!
马里奥·加西亚

6

这是使测试具有表现力的问题。如果希望测试描述两个对象之间的关系,我会对模拟模型设定期望。如果要设置一个支持对象以使我了解测试中的有趣行为,那么我会返回值。


6

如果您熟悉Arrange-Act-Assert,那么解释存根和模拟之间的差异可能对您有用的一种方法是,存根属于安排部分,因为它们是用于排列输入状态的,而模拟属于断言部分,因为它们用于断言结果。

假人什么也没做。它们仅用于填充参数列表,因此不会出现未定义或空错误。它们也可以满足严格类型化语言中的类型检查器的要求,因此可以允许您编译和运行它们。


3

Stub,Fakes和Mocks在不同来源具有不同的含义。我建议您介绍团队内部术语并同意其含义。

我认为区分两种方法很重要:-行为验证(暗示行为替代)-终端状态验证(暗示行为仿真)

如果发生错误,请考虑发送电子邮件。进行行为验证时-您检查的方法Send是否IEmailSender已执行一次。并且您需要模拟此方法的返回结果,返回已发送消息的ID。所以你说:“我希望Send会被调用。对于任何调用,我只会返回虚拟(或随机)ID”。这是行为验证: emailSender.Expect(es=>es.Send(anyThing)).Return((subject,body) => "dummyId")

在进行状态验证时,您将需要创建TestEmailSender该实现IEmailSender。并实施Send方法-通过将输入保存到某些数据结构中,这些数据结构将用于将来的状态验证(例如某些对象的数组),SentEmails然后进行测试,您将检查其中是否SentEmails包含预期的电子邮件。这是状态验证: Assert.AreEqual(1, emailSender.SentEmails.Count)

从我的阅读中,我了解到行为验证通常称为Mocks。和状态验证通常被称为存根假货


确实非常详细和清晰的定义。
shyam sundar singh tomar,

2

存根对象是对象,因为它们可以根据输入参数改变响应。它们之间的主要区别是Fake比存根更接近真实世界的实现。存根基本上包含对预期请求的硬编码响应。让我们看一个例子:

public class MyUnitTest {

 @Test
 public void testConcatenate() {
  StubDependency stubDependency = new StubDependency();
  int result = stubDependency.toNumber("one", "two");
  assertEquals("onetwo", result);
 }
}

public class StubDependency() {
 public int toNumber(string param) {
  if (param == “one”) {
   return 1;
  }
  if (param == “two”) {
   return 2;
  }
 }
}

模拟是从假货和存根升压。模拟提供与存根相同的功能,但更为复杂。他们可以为他们定义规则,这些规则规定必须调用API上的方法的顺序。大多数模拟都可以跟踪方法被调用的次数,并可以根据该信息做出反应。嘲笑通常知道每个呼叫的上下文,并且在不同情况下可以做出不同的反应。因此,模拟需要他们正在模拟的类的一些知识。存根通常无法跟踪方法的调用次数或方法序列的调用顺序。模拟看起来像:

public class MockADependency {

 private int ShouldCallTwice;
 private boolean ShouldCallAtEnd;
 private boolean ShouldCallFirst;

 public int StringToInteger(String s) {
  if (s == "abc") {
   return 1;
  }
  if (s == "xyz") {
   return 2;
  }
  return 0;
 }

 public void ShouldCallFirst() {
  if ((ShouldCallTwice > 0) || ShouldCallAtEnd)
   throw new AssertionException("ShouldCallFirst not first thod called");
  ShouldCallFirst = true;
 }

 public int ShouldCallTwice(string s) {
  if (!ShouldCallFirst)
   throw new AssertionException("ShouldCallTwice called before ShouldCallFirst");
  if (ShouldCallAtEnd)
   throw new AssertionException("ShouldCallTwice called after ShouldCallAtEnd");
  if (ShouldCallTwice >= 2)
   throw new AssertionException("ShouldCallTwice called more than twice");
  ShouldCallTwice++;
  return StringToInteger(s);
 }

 public void ShouldCallAtEnd() {
  if (!ShouldCallFirst)
   throw new AssertionException("ShouldCallAtEnd called before ShouldCallFirst");
  if (ShouldCallTwice != 2) throw new AssertionException("ShouldCallTwice not called twice");
  ShouldCallAtEnd = true;
 }

}

1

fake object是接口(协议)的一实实现或使用继承或可用于创建其他方法扩展依赖。通常,它是由开发人员创建的最简单的解决方案,以替代某些依赖项

stub object是一个裸对象(0,nil和没有逻辑的方法),具有多余和预定义(开发人员)状态来定义返回值。通常是由框架创建

mock object与非常相似,stub object但是在程序执行期间会更改额外的状态,以检查是否发生了某些事情(调用了方法)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.