装逼,嘲讽,存根有什么区别

我知道我如何使用这些术语,但我想知道对于单元测试,是否有伪造嘲笑存根的公认定义?你如何为你的测试定义这些?描述你可能使用它们的情况。

以下是我如何使用它们:

:实现接口但包含固定数据而没有逻辑的类。根据实现简单地返回“好”或“坏”数据。

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

存根:类似于模拟类,除了它不提供验证方法已被调用/未被调用的能力。

模拟和存根可以手动生成,也可以由模拟框架生成。假类是手动生成的。我使用模拟主要是为了验证我的类和依赖类之间的交互。一旦我验证了交互,我就会使用存根,并且正在通过我的代码测试替代路径。我使用假类主要是为了抽象出数据依赖关系,或者当模拟/存根每次设置都太乏味时。

267552 次浏览

你可以得到一些信息:

Martin Fowler关于Mock and Stub的文章

对象实际上有工作实现,但通常会采取一些捷径,这使得它们不适合生产

存根为测试期间的调用提供预置答案,通常根本不响应为测试编程的内容之外的任何内容。存根还可以记录有关调用的信息,例如记住它“发送”的消息的电子邮件网关存根,或者可能只记住它“发送”的消息数量。

嘲笑是我们在这里谈论的:对象预先编程了期望,这些期望形成了它们期望接收的调用的规范。

xunit模式

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

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

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

个人而言

我尝试使用:Mock和Stub来简化。当它是一个返回设置为测试类的值的对象时,我使用Mock。我使用Stub来模拟要测试的接口或抽象类。事实上,你叫它什么并不重要,它们都是在生产中没有使用的类,并且用作测试的实用程序类。

这是一个让测试富有表现力的问题。如果我想让测试描述两个对象之间的关系,我会在Mock上设置期望。如果我要设置一个支持对象来让我了解测试中有趣的行为,我会存根返回值。

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

Mock-您设置期望的对象。

-功能有限的对象(用于测试),例如虚假的Web服务。

双倍测试是存根、模拟和假货的总称。但非正式地,你会经常听到人们简单地称之为模拟。

我很惊讶这个问题已经存在了这么久,还没有人根据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上的值,使该伪造成为模拟。

当然,这些例子都是精心设计的,但我认为这种区别有很大的好处。它让你意识到你是如何测试你的东西的,以及测试的依赖关系在哪里。

我同意Osherove的观点

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

针对假的断言是你真正想避免的事情,因为它使你的测试高度依赖于根本不是被测类的类的实现。这意味着类ActualClassUnderTest的测试可能开始中断,因为ClassUsedAsMock的实现发生了变化。这给我带来了难闻的气味。ActualClassUnderTest的测试最好只有在ActualClassUnderTest发生变化时才会中断。

我意识到编写针对虚假的断言是一种常见的做法,尤其是当你是一个嘲弄类型的TDD订阅者时。我想我坚决支持Martin Fowler在古典主义阵营中(参见马丁·福勒的“嘲笑不是存根”),并像Osherove一样尽可能避免交互测试(这只能通过针对虚假的断言来完成)。

如果你想了解为什么你应该避免这里定义的嘲笑,谷歌搜索“福勒嘲笑古典主义者”。你会发现大量的意见。

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

傻瓜什么也不做。它们只是用来填充参数列表,这样你就不会出现未定义或空错误。它们的存在也是为了满足静态类型语言中的类型检查器,这样你就可以编译和运行。

为了说明存根和模拟的用法,我还想包括一个基于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 = stubServicelog.Email=mockEmail;string tooShortFileName="abc.ext";log.Analyze(tooShortFileName);
Assert.AreEqual("a",mockEmail.To); //MOCKING USEDAssert.AreEqual("fake exception",mockEmail.Body); //MOCKING USEDAssert.AreEqual("subject",mockEmail.Subject);}}

你在上面断言的东西叫做模拟对象。

帮助测试运行的其他所有内容都是存根

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

public class MyUnitTest {
@Testpublic 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;}
}

正如投票最多的答案所提到的,Martin Fowler在嘲笑不是存根中讨论了这些区别,特别是副标题Mocks和Stubs的区别,所以一定要阅读那篇文章。

与其关注如何,这些事情是不同的,我认为关注为什么更有启发性这些是不同的概念。每一个都有不同的目的。

假货

假的是一个行为“自然”但不是“真实”的实现。这些是模糊的概念,因此不同的人对什么使事物成为假的有不同的理解。

伪造的一个例子是内存数据库(例如将sqlite与:memory:存储一起使用)。你永远不会将其用于生产(因为数据不会持久化),但它作为数据库在测试环境中使用是完全足够的。它也比“真实”数据库轻量级得多。

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

作为第三个例子,想象一个提供缓存API的对象;一个实现了正确接口但根本不执行缓存但总是返回缓存未命中的对象将是一种伪造。

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

存根

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

存根的目的是让您的系统进入特定状态。例如,如果您正在为与REST API交互的某些代码编写测试,您可以存根 REST API使用始终返回预制响应的API,或者响应带有特定错误的API请求。这样您就可以编写测试来断言系统如何对这些状态做出反应;例如,如果API返回404错误,测试用户获得的响应。

存根通常实现为仅响应您告诉它响应的确切交互。但是使某物成为存根的关键特性是它的目的:存根就是设置您的测试用例。

嘲笑

模拟类似于存根,但在模拟的目的是断言您的被测系统如何与依赖项交互中添加了验证

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

模拟与交互测试相关,这是一个特定的测试方法论。喜欢测试系统状态而不是系统交互的人会很少使用模拟。

测试双打

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

Stub,Fakes和Mocks在不同的来源有不同的含义。我建议你介绍你的团队内部术语,并就它们的含义达成一致。

我认为区分两种方法很重要:-行为验证(意味着行为替代)-结束状态验证(意味着行为模拟)

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

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

从我的阅读中,我明白行为验证通常称为嘲笑状态验证通常称为存根假货

Unit testing-是一种测试单元(类、方法)是否在控制之下的方法。

Test double-不是主要对象(来自OOP世界)。它是临时为测试、检查或开发期间创建的实现。它们是为关闭依赖项的测试单元(方法、类…)创建的

测试双打类型:

  • fake object是接口(协议)的真正实施延长,它使用继承或其他方法来创建-is依赖项。通常它是由开发人员创建的,作为替代某些依赖项的最简单解决方案

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

class StubA: A {override func foo() -> String {return "My Stub"}}
  • mock objectstub object非常相似,但额外的状态在程序执行期间被更改以检查出事了(方法被调用,参数,何时,多久…)。
class MockA: A {var isFooCalled = falseoverride func foo() -> String {isFooCalled = truereturn "My Mock"}}
  • spy object是一个具有“部分模拟”的真实对象。这意味着您使用非双对象,除了模拟行为

  • dummy object是运行测试所必需的对象,但不会调用该对象的任何变量或方法。

存根vs模拟

Martin Fowler说

不同之处在于存根使用状态验证,而模拟使用行为验证。

[Mockito模拟vs间谍]

所有这些都称为Test Double,用于注入测试用例所需的依赖项。

伪造

存根:它已经有一个预定义的行为来设置你的期望例如,存根仅返回API响应的成功案例Stub

mock是一个更智能的存根。您验证您的测试通过它。因此,您可以使amock返回成功或失败,成功取决于条件,可以在您的测试用例中更改。Mock

虚拟

间谍

在Gerard Meszaros的xUnit测试模式书中,有一个很好的表格,可以很好地了解差异

输入图片描述

根据Vladimir Khorikov的《单元测试原则、实践和模式》一书:

  • 嘲笑:帮助模拟和检查输出交互。这些交互是SUT对其依赖项进行的调用以更改它们的状态。换句话说,它有助于检查SUT及其依赖项的交互(行为)。模拟可以是:
    1. Spy:手动创建
    2. Mocks:使用框架创建
  • 存根:有助于模拟传入交互。这些交互是SUT对其依赖项进行的调用以获取输入数据。换句话说,它有助于测试传递给SUT的数据。它可以是3种类型
    1. Fake:通常用于替换尚不存在的依赖项。
    2. 虚拟:是硬编码的值。
    3. 存根:您配置为针对不同方案返回不同值的已启动依赖项。