【问题标题】:What's the difference between faking, mocking, and stubbing?伪造,嘲笑和存根之间有什么区别?
【发布时间】:2010-09-25 16:08:25
【问题描述】:

我知道我如何使用这些术语,但我想知道对于 fakingmockingstubbing 是否有公认的定义单元测试?你如何为你的测试定义这些?描述您可能会使用每种情况的情况。

我是这样使用它们的:

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

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

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

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

【问题讨论】:

  • 嗯,你基本上在你的“问题”中说了这一切:) 我认为这些术语的定义非常好
  • 维基百科对 Fake 的定义与此不同,声称 Fake “用作更简单的实现,例如在测试中使用内存数据库而不是进行真正的数据库访问)”参见 @987654321 @
  • 我从以下资源中学到了很多,Robert C. Martin(鲍勃叔叔)的精彩解释:The Little Mocker on The Clean Code Blog。它解释了假人、测试替身、存根、间谍、(真正的)模拟和假货之间的区别和微妙之处。它还提到了 Martin Fowler,并解释了一些软件测试历史。
  • testing.googleblog.com/2013/07/…(简短的一页摘要)。
  • 这是我的解释:Test Doubles: Fakes, Stubs and Mocks(带有示例的博客文章)

标签: unit-testing mocking language-agnostic terminology definition


【解决方案1】:

你可以得到一些信息:

来自Martin Fowler about Mock and Stub

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

存根为测试期间拨打的电话提供预设答案,通常根本不响应任何超出测试程序的内容。存根还可以记录有关呼叫的信息,例如一个电子邮件网关存根,它记住它“发送”的消息,或者可能只记住它“发送”了多少条消息。

Mocks 就是我们在此讨论的内容:预编程了期望的对象,这些期望形成了它们期望接收的调用的规范。

来自xunitpattern

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

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

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

个人

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

【讨论】:

  • 在我看来,与 Martin Fowler 的引用相比,xUnitPattern 引用中 Stub 和 Fake 的定义是相反的。此外,与 tvanfosson 原始问题中的定义相比,Martin Fowler 对 Stub 和 Fake 的定义是相反的。实际上,这两个术语是否有任何普遍接受的定义,还是仅取决于您在与谁交谈?
  • +1 表示“我尝试通过使用:模拟和存根来简化”。这是个好主意!
  • 看不出只使用 Mock 和 Stub 是一个好主意。每个测试替身都有其目的,因此也有其用途。
  • 在MF的定义中我看不出Fake和Mock的区别。
  • @MusuNaji:在 MF 的定义中,对于 Fake 的对话没有“期望”,除了它的接口有一个实现。另一方面,Mock 将受到挑战(是否调用了此方法?)。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

    Mock - 您设定期望的对象。

    Fake - 功能有限的对象(用于测试目的),例如一个虚假的网络服务。

    Test Double 是 stub、mock 和 fake 的总称。但非正式地,你会经常听到人们简单地称他们为嘲笑者。

    【讨论】:

    • 谁能向我解释和定义在这种情况下什么是“固定答案”?
    • 一个明确的值,而不是计算出来的值。
    • 终于!一些我能理解的定义!然后,基于这些定义,googletest (gtest) / googlemock (gmock) 允许模拟对象也成为存根,因为您可以在模拟方法上创建 EXPECT_CALL()s,该方法通过使用 .WillOnce(Invoke(my_func_or_lambda_func))(或使用 @987654326 @) 类型语法附加到 EXPECT_CALL()。使用Invoke() 的一些示例可以在我的长答案底部的不同上下文中看到:stackoverflow.com/a/60905880/4561887
    • Invoke() 上的 Gmock 文档在这里:github.com/google/googletest/blob/master/googlemock/docs/…。无论如何,结论是:Google mock (gmock) 允许人们轻松地创建 mocks stubs,尽管大多数 mocks 不是 stubs。
    • Mocks 是 Stubs 的超集,它们仍然可以返回预定义的答案,但也允许开发人员设定期望。 IMO 那里的某些库模糊了所有测试假人的界限。
    【解决方案4】:

    我很惊讶这个问题已经存在了这么久,还没有人根据Roy Osherove's "The Art of Unit Testing"提供答案。

    在“3.1 引入存根”中将存根定义为:

    存根是现有依赖项的可控替换 (或合作者)在系统中。通过使用存根,您可以在没有 直接处理依赖关系。

    并将 stubs 和 mocks 的区别定义为:

    关于 mock 与 stub 的主要区别在于,mock 就像 stub 一样,但是你对 mock 对象进行断言,而不是对 stub 进行断言。

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

    Osherove 区分 stub 和 mock 的方式意味着,任何用作测试的伪类都可以是 stub 或 mock。它用于特定测试完全取决于您如何在测试中编写检查。

    • 当您的测试检查被测类中的值时,或者实际上是除了假的任何地方的值时,假的被用作存根。它只是提供值供被测类使用,可以直接通过调用返回的值,也可以间接通过调用导致副作用(在某些状态下)。
    • 当您的测试检查虚假值时,它被用作模拟。

    将 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 上的值,从而使该假冒成为模拟。

    当然,这些例子是高度人为的,但我认为这种区别有很大的好处。它让你知道你是如何测试你的东西的,以及你的测试的依赖关系在哪里。

    我同意奥舍罗夫的观点

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

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

    我意识到针对虚假情况编写断言是一种常见的做法,尤其是当您是 Mockist 类型的 TDD 订阅者时。我想我在古典主义阵营中坚定地支持 Martin Fowler(参见 Martin Fowler's "Mocks aren't Stubs"),并且像 Osherove 一样尽可能避免交互测试(这只能通过对虚假的断言来完成)。

    为了有趣地阅读为什么你应该避免这里定义的模拟,谷歌搜索“fowler mockist classicist”。你会发现很多意见。

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:

        为了说明存根和模拟的用法,我还想包含一个基于 Roy Osherove 的“The Art of Unit Testing”的示例。

        想象一下,我们有一个 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 服务来验证测试结果,只是为了确保测试正确运行。电子邮件服务是一个模拟,因为我们会针对它断言它被正确调用。

        [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);
         }
        }
        

        【讨论】:

          【解决方案7】:

          您在其上声明的东西称为 mock 对象,而其他任何有助于测试运行的东西都是 stub

          【讨论】:

          • 而其他答案非常详细并且非常好。这使得改变变得如此清晰和容易,很难不投票。 gj!
          【解决方案8】:

          stubfake 是对象,因为它们可以根据输入参数改变响应。它们之间的主要区别在于,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;
            }
           }
          }
          

          mock 是假货和存根的升级版。模拟提供与存根相同的功能,但更复杂。他们可以为他们定义规则,规定必须以什么顺序调用 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;
           }
          
          }
          

          【讨论】:

            【解决方案9】:

            正如投票率最高的答案所述,Martin Fowler 在Mocks Aren't Stubs 中讨论了这些区别,特别是在子标题The Difference Between Mocks and Stubs 中,因此请务必阅读该文章。

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

            假货

            fake 是一种行为“自然”但不是“真实”的实现。这些都是模糊的概念,所以不同的人对什么是假货有不同的理解。

            一个伪造的例子是内存数据库(例如,使用带有:memory: 存储的sqlite)。你永远不会将它用于生产(因为数据没有被持久化),但它完全可以作为在测试环境中使用的数据库。它也比“真正的”数据库轻得多。

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

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

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

            存根

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

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

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

            模拟

            mock 类似于存根,但添加了 verificationmock 的目的是断言您的被测系统如何交互有依赖关系

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

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

            测试双打

            Fakes、stubs 和 mocks 都属于 test doubles 的类别。测试替身是您在测试中使用的任何对象或系统而不是其他东西。大多数自动化软件测试都涉及使用某种类型的测试替身。其他一些测试替身包括虚拟值间谍和 I/O 黑洞

            【讨论】:

            • 我已阅读所有答案。我认为这是迄今为止最好的说明。
            【解决方案10】:

            Stub、Fakes 和 Mocks 在不同的来源中具有不同的含义。我建议你介绍一下你的团队内部术语并同意它们的含义。

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

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

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

            根据我的阅读,我了解到行为验证通常称为Mocks。 而状态验证通常称为StubsFakes

            【讨论】:

            • 非常详细和清晰的定义。
            【解决方案11】:

            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 = false
                override func foo() -> String {
                    isFooCalled = true
                    return "My Mock"
                }
            }
            
            • spy object 是一个带有“部分模拟”的真实对象。这意味着您使用 non-double 对象,除了模拟行为

            • dummy object 是运行测试所必需的对象,但没有一个变量或该对象的方法未被调用。

            存根与模拟

            Martin Fowler said

            stub 使用状态验证,mock 使用行为验证。

            [Mockito mock vs spy]

            【讨论】:

              【解决方案12】:

              它们都称为测试替身,用于注入测试用例所需的依赖项。

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

              模拟是更智能的存根。您验证您的测试通过它。 因此,您可以根据条件在测试用例中更改返回成功或失败成功的 amock。

              【讨论】:

                【解决方案13】:

                在 Gerard Meszaros 的 xUnit Test Patterns 书中,有一张很好的表格可以很好地洞察差异

                【讨论】:

                  【解决方案14】:

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

                  • 模拟:帮助模拟和检查即将发生的交互。这些交互是 SUT 对其依赖项进行的调用以更改其状态。换句话说,它有助于检查 SUT 的交互(行为)及其依赖关系。模拟可能是:
                    1. 间谍:手动创建
                    2. 模拟:使用框架创建
                  • 存根:有助于模拟传入的交互。这些交互是 SUT 对其依赖项进行的调用以获取输入数据。换句话说,它有助于测试传递给 SUT 的数据。可以是3种
                    1. Fake:通常用于替换尚不存在的依赖项。
                    2. Dummy:是硬编码值。
                    3. 存根:您配置为针对不同场景返回不同值的成熟依赖项。

                  【讨论】:

                  • 如果有人想知道 SUT 是什么,那就是“被测系统”。
                  猜你喜欢
                  • 2011-02-09
                  • 1970-01-01
                  • 1970-01-01
                  • 2017-06-11
                  • 2012-11-21
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多