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

nev*_*ame 884 testing mocking stub

我已经阅读过各种关于模拟和测试中存根的文章,包括Martin Fowler的Mocks Are Not Stubs,但仍然不明白其中的区别.

Rys*_*gan 813

前言

对象有几种定义,不是真实的.一般术语是测试双倍.这个术语包括:虚拟,,存根,模拟.

参考

根据Martin Fowler的文章:

  • 虚拟对象传递但从未实际使用过.通常它们仅用于填充参数列表.
  • 对象实际上有工作实现,但通常需要一些使它们不适合生产的快捷方式(内存数据库就是一个很好的例子).
  • 存根提供了在测试期间进行的调用的固定答案,通常不会对测试中编程的任何内容做出任何响应.存根还可以记录有关呼叫的信息,例如记住它'发送'的消息的电子邮件网关存根,或者可能只记录它'发送'的消息.
  • 模拟是我们在这里所讨论的:预编程的对象具有预期,形成了预期接收的调用的规范.

样式

Mocks vs Stubs =行为测试与状态测试

原理

根据每次测试只测试一件事的原则,在一次测试中可能有几个存根,但通常只有一个模拟.

生命周期

使用存根测试生命周期:

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

使用模拟测试生命周期:

  1. 设置数据 - 准备正在测试的对象.
  2. 设置期望 - 准备主要对象正在使用的模拟期望.
  3. 练习 - 测试功能.
  4. 验证期望 - 验证是否已在mock中调用了正确的方法.
  5. 验证状态 - 使用断言检查对象的状态.
  6. 拆解 - 清理资源.

摘要

模拟和存根测试都给出了问题的答案:结果是什么?

使用模拟测试也对以下方面感兴趣:结果如何实现?


Sea*_*ver 691

存根

我相信最大的区别是你已经用预定的行为编写了一个存根.所以你会有一个实现依赖的类(最有可能的抽象类或接口),你为了测试目的而伪装,这些方法只是用set响应来删除.他们不会做任何花哨的事情,你会在测试之外编写它的存根代码.

嘲笑

模拟是你的测试的一部分,你必须设置你的期望.模拟不是以预定方式设置的,因此您可以在测试中使用代码.某种方式的模拟是在运行时确定的,因为设置期望的代码必须在它们执行任何操作之前运行.

区别

用模拟编写的测试通常遵循initialize -> set expectations -> exercise -> verify测试模式.虽然预先编写的存根将遵循initialize -> exercise -> verify.

相似

两者的目的是消除测试类或函数的所有依赖关系,以便您的测试更加集中,更简单,他们试图证明.

  • 一句令人困惑的台词——“模拟是作为测试的一部分,你必须按照你的期望进行设置的东西。” 模拟不是以预定的方式设置的,因此您有在测试中执行此操作的代码。所以,它是根据您的期望设置的,但不是以预定的方式设置?这怎么可能? (4认同)

Arn*_*psa 324

存根是简单的假物体.它只是确保测试顺利进行.
模拟是更聪明的存根.您验证您的测试通过它.

  • 我认为这是最简洁的答案.外卖:模拟IS-A存根.http://stackoverflow.com/a/17810004/2288628是这个答案的较长版本. (24认同)
  • 我不认为模拟是一个存根.模拟用于断言并且永远不应返回数据,存根用于返回数据并且永远不应断言. (6认同)
  • 我认为这很棒-存根返回问题的答案。模拟还返回问题的答案(是存根),但也可以验证问题是否存在!! (6认同)
  • @dave1010 Mocks 绝对可以返回数据甚至抛出异常。他们应该这样做以响应传递给他们的参数。 (4认同)
  • @trenton如果一个对象根据传入的数据返回或抛出,那么它就是*fake*,而不是mock.Stubs测试你的SUT如何处理*接收*消息,模拟测试你的SUT*如何发送*消息.混合2可能会导致糟糕的OO设计. (2认同)
  • 这个解释可能有点误导,因为“fake”是另一种测试替身的名称。 (2认同)

Lev*_*Lev 225

以下是对每个示例的描述,然后是真实世界的示例.

  • 虚拟 - 只是虚假的价值来满足API.

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

  • - 创建一个可能依赖于某些外部基础结构的类的测试实现.(这是很好的做法,你的单元测试也不要实际上外部基础设施交互.)

    示例:创建用于访问数据库的虚假实现,将其替换为in-memory集合.

  • 存根 - 覆盖返回硬编码值的方法,也称为state-based.

    示例:您的测试类取决于Calculate()需要5分钟才能完成的方法.您可以使用返回硬编码值的存根替换其实际实现,而不是等待5分钟; 只占一小部分时间.

  • 模拟 - 非常相似Stubinteraction-based不是基于状态.这意味着您不希望Mock返回某个值,而是假设方法调用的特定顺序.

    示例:您正在测试用户注册类.打电话后Save,应该打电话SendConfirmationEmail.

Stubs并且Mocks实际上是子类型Mock,包括交换实际实现和测试实现,但出于不同的,特定的原因.

  • **1) 阅读答案 2) 阅读 https://blog.cleancoder.com/uncle-bob/2014/05/14/TheLittleMocker.html 3) 再次阅读答案。** (2认同)
  • “存根和模拟实际上是模拟的子类型”。我认为你的意思是“存根和模拟实际上是假的子类型”。 (2认同)

小智 168

codeschool.com课程中,Rails Testing for Zombies,他们给出了这些术语的定义:

存根

用于使用返回指定结果的代码替换方法.

嘲笑

一个断言,断言该方法被调用.

正如Sean Copenhaver在他的回答中所描述的那样,区别在于模拟设定了期望(即做出断言,关于是否或如何被调用).


mk_*_*mk_ 133

存根不会使您的测试失败,模拟可以.

  • 我认为这很好,您知道重构后测试是否具有相同的行为。 (2认同)

Ghi*_*nio 33

我认为关于这个问题的最简单和更清晰的答案来自Roy Osherove在他的书"单位测试的艺术"(第85页)中

告诉我们处理存根的最简单方法是注意存根永远不会使测试失败.断言测试用途总是针对被测试的类.

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

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

这意味着如果你正在对假冒进行断言,这意味着你使用假冒作为模拟,如果你只使用假冒运行测试而没有断言,你使用假冒作为存根.

  • 我希望你的答案能够找到最佳状态.这是R. Osherove解释这个https://youtu.be/fAb_OnooCsQ?t=1006. (2认同)

O'R*_*ney 26

阅读上面的所有解释,让我试着浓缩:

  • 存根:一段允许测试运行的虚拟代码,但您不关心它发生了什么.
  • 模拟:一段虚拟代码,作为测试的一部分,您可以正确调用VERIFY.
  • 间谍:一段虚拟代码,拦截对真实代码的一些调用,允许您在不替换整个原始对象的情况下验证调用.

  • 好答案.根据你的定义,Mock听起来与Spy非常相似.如果你更新你的答案以包括更多的测试双打,那将是很好的. (2认同)

Neb*_*Fox 22

模拟只是测试行为,确保调用某些方法.Stub是特定对象的可测试版本(本身).

你是什​​么意思Apple方式?

  • "你用Apple的方式是什么意思?" 使用Helvetica (17认同)
  • 以Apple方式而不是Microsoft方式:) (6认同)
  • 这对情况有帮助吗? (2认同)

hap*_*ore 20

如果将它与调试进行比较:

Stub就像确保方法返回正确的值一样

模拟实际上就像进入方法并确保内部的所有内容都正确,然后返回正确的值.


Joe*_*ang 19

我认为他们之间最重要的区别是他们的意图.

让我试着在WHY stubWHY mock中解释它

假设我正在为我的mac twitter客户端的公共时间线控制器编写测试代码

这是测试示例代码

twitter_api.stub(:public_timeline).and_return(public_timeline_array)
client_ui.should_receive(:insert_timeline_above).with(public_timeline_array)
controller.refresh_public_timeline
Run Code Online (Sandbox Code Playgroud)
  • STUB:与twitter API的网络连接非常慢,这使我的测试变慢.我知道它会返回时间轴,所以我创建了一个模拟HTTP twitter API的存根,这样我的测试就可以非常快地运行,即使我离线也可以运行测试.
  • MOCK:我还没有编写任何我的UI方法,而且我不确定我需要为我的ui对象编写什么方法.我希望通过编写测试代码来了解我的控制器如何与我的ui对象协作.

通过编写mock,您可以通过验证期望得到满足来发现对象协作关系,而stub只模拟对象的行为.

如果你想了解更多关于模拟的信息,我建议你阅读这篇文章:http://jmock.org/oopsla2004.pdf


R01*_*010 17

要非常清楚和实用:

Stub:一个类或对象,它实现要伪造的类/对象的方法,并始终返回您想要的内容.

JavaScript中的示例:

var Stub = {
   method_a: function(param_a, param_b){
      return 'This is an static result';
   }
}
Run Code Online (Sandbox Code Playgroud)

模拟:存根相同,但它添加了一些逻辑,在调用方法时"验证",因此您可以确定某些实现正在调用该方法.

正如@mLevan所说,想象一下您正在测试用户注册类.调用Save后,应该调用SendConfirmationEmail.

一个非常愚蠢的代码示例:

var Mock = {
   calls: {
      method_a: 0
   }

   method_a: function(param_a, param_b){
     this.method_a++; 
     console.log('Mock.method_a its been called!');
   }
}
Run Code Online (Sandbox Code Playgroud)


Mos*_*sho 16

使用心理模型确实帮助我理解了这一点,而不是所有的解释和文章,并没有完全"沉入".

想象一下,你的孩子桌子上有一个玻璃盘,他开始玩它.现在,你担心它会破裂.所以,你给他一块塑料板.这将是一个模拟(相同的行为,相同的界面,"更软"的实现).

现在,假设你没有塑料更换,所以你解释"如果你继续玩它,它会破裂!".这是一个Stub,你事先提供了一个预定义的状态.

一个假人将他甚至没有用叉子...和间谍可能是这样提供的工作你已经使用了相同的解释.一个会像卧,如果他继续警察会来(不知道最后一部分:).


小智 15

模拟:帮助模拟和检查结果交互。这些交互是 SUT 对其依赖项进行调用以更改其状态。

存根:帮助模拟传入的交互。这些交互是 SUT 对其依赖项进行调用以获取输入数据。

在此输入图像描述

来源:单元测试原则、实践和模式 - Manning


Avi*_*ger 14

这张幻灯片解释了主要的差异非常好.

在此输入图像描述

*来自华盛顿大学CSE 403第16讲(由"Marty Stepp"创作的幻灯片)


nit*_*gar 11

我喜欢Roy Osherove发布的解释[视频链接].

创建的每个类或对象都是假的.如果您验证对它的呼叫,它是一个模拟.否则它是一个存根.


Rel*_*ros 11

  • Stubs vs. Mocks
    • 存根
      1. 提供方法调用的具体答案
        • 例如:myStubbedService.getValues()只返回被测代码所需的字符串
      2. 被测试的代码用来隔离它
      3. 不能失败测试
        • 例如:myStubbedService.getValues()只返回存根值
      4. 经常实现抽象方法
    • 嘲弄
      1. 存根的"超集"; 可以断言某些方法被调用
        • 例如:验证myMockedService.getValues()仅被调用一次
      2. 用于测试被测代码的行为
      3. 可以通过测试失败
        • 例:验证myMockedService.getValues()被调用一次; 验证失败,因为我的测试代码没有调用myMockedService.getValues()
      4. 经常嘲笑接口


Afo*_*tos 10

我正在阅读单元测试的艺术,并偶然发现了以下定义:

一个是可以用来描述任何存根或模拟对象(手写或其他方式)的总称,因为他们看起来像真正的对象。假是存根还是模拟取决于它在当前测试中的使用方式。如果它用于检查交互(断言),则它是一个模拟对象。否则,它是一个存根


Mus*_*ici 9

一个是可以用来描述任何存根或模拟对象(手写或其他方式),因为他们看起来像真正的对象的总称.

假的是存根还是模拟取决于它在当前测试中的使用方式.如果它用于检查交互(断言),则它是一个模拟对象.否则,它是一个存根.

假货确保测试顺利进行.这意味着未来测试的读者将理解虚假对象的行为,而无需阅读其源代码(无需依赖外部资源).

测试运行顺畅意味着什么?
例如下面的代码:

 public void Analyze(string filename)
        {
            if(filename.Length<8)
            {
                try
                {
                    errorService.LogError("long file entered named:" + filename);
                }
                catch (Exception e)
                {
                    mailService.SendEMail("admin@hotmail.com", "ErrorOnWebService", "someerror");
                }
            }
        }
Run Code Online (Sandbox Code Playgroud)

你想测试mailService.SendEMail()方法,为了做到这一点你需要在你的测试方法中模拟一个Exception,所以你只需要创建一个Fake Stub errorService类来模拟那个结果,然后你的测试代码就可以测试了mailService.SendEMail()方法.如您所见,您需要模拟来自另一个外部依赖性ErrorService类的结果.


Ali*_*ili 9

让我看看测试双打:

  • :伪造是具有工作实现的对象,但与生产实现不同.:数据访问对象或存储库的内存实现.
  • 存根:存根是一个保存预定义数据的对象,并在测试期间使用它来应答调用.:一个对象需要从数据库中获取一些数据以响应方法调用.

  • 模拟:模拟是注册接收呼叫的对象.在测试断言中,我们可以在Mocks上验证所有预期的操作都已执行.:调用电子邮件发送服务的功能.更多只是检查一下.

  • 我认为最好的答案 (2认同)

Mur*_*dız 9

存根

\n

存根是一个保存预定义数据并在测试期间使用它来应答调用的对象。当您可以\xe2\x80\x99t或不想\xe2\x80\x99t想要涉及将用真实数据回答或具有不良副作用的对象时,可以使用它。

\n

一个示例可以是需要从数据库获取一些数据以响应方法调用的对象。我们引入了一个存根并定义了应该返回哪些数据,而不是真正的对象。

\n

在此输入图像描述

\n
\n

存根示例:

\n
public class GradesService {\n\n   private final Gradebook gradebook;\n\n   public GradesService(Gradebook gradebook) {\n       this.gradebook = gradebook;\n   }\n\n   Double averageGrades(Student student) {\n       return average(gradebook.gradesFor(student));\n   }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

您无需从成绩册商店调用数据库来获取真实的学生成绩,而是预先配置存根并将返回成绩。您定义了足够的数据来测试平均计算算法。

\n
public class GradesServiceTest {\n\n   private Student student;\n   private Gradebook gradebook;\n\n   @Before\n   public void setUp() throws Exception {\n       gradebook = mock(Gradebook.class);\n       student = new Student();\n   }\n\n   @Test\n   public void calculates_grades_average_for_student() {\n       //stubbing gradebook\n       when(gradebook.gradesFor(student)).thenReturn(grades(8, 6, 10)); \n\n       double averageGrades = new GradesService(gradebook).averageGrades(student);\n\n       assertThat(averageGrades).isEqualTo(8.0);\n   }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
\n
\n

嘲笑

\n

模拟是注册它们收到的调用的对象。在测试断言中,您可以在模拟上验证是​​否执行了所有预期操作。当您不想调用生产代码或者没有简单的方法来验证预期代码是否已执行时,您可以使用模拟。没有返回值,也没有简单的方法来检查系统状态更改。一个示例可以是调用电子邮件发送服务的功能。

\n

您不想\xe2\x80\x99 每次运行测试时都发送电子邮件。此外,在测试中验证是否发送了正确的电子邮件并不容易。您唯一能做的就是验证我们测试中所执行功能的输出。在其他世界中,验证是否调用了电子邮件发送服务。

\n

在此输入图像描述

\n
\n

模拟示例:

\n
public class SecurityCentral {\n\n   private final Window window;\n   private final Door door;\n\n   public SecurityCentral(Window window, Door door) {\n       this.window = window;\n       this.door = door;\n   }\n\n   void securityOn() {\n       window.close();\n       door.close();\n   }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

您不想\xe2\x80\x99 想要关闭真正的门来测试安全方法是否有效,对吧?相反,您将门和窗模拟对象放置在测试代码中。

\n
public class SecurityCentralTest {\n\n   Window windowMock = mock(Window.class);\n   Door doorMock = mock(Door.class);\n\n   @Test\n   public void enabling_security_locks_windows_and_doors() {\n       SecurityCentral securityCentral = new SecurityCentral(windowMock, doorMock);\n\n       securityCentral.securityOn();\n\n       verify(doorMock).close();\n       verify(windowMock).close();\n   }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

非常感谢 Micha\xc5\x82 Lipski 的好文章。进一步阅读:

\n

测试替身 \xe2\x80\x93 Martin Fowler https://martinfowler.com/bliki/TestDouble.html
\n测试替身 \xe2\x80\x93 xUnit 模式http://xunitpatterns.com/Test%20Double.html
\nMocks Aren \xe2\x80\x99t 存根 \xe2\x80\x93 Martin Fowler https://martinfowler.com/articles/mocksArentStubs.html
\n命令查询分离 \xe2\x80\x93 Martin Fowler https://martinfowler.com/bliki/ CommandQuerySeparation.html

\n


Dim*_*mos 7

正好来自纸模拟角色,而不是对象,由jMock的开发人员:

存根是生成代码的虚拟实现,可返回固定结果.模拟对象充当存根,但也包括用于检测目标对象与其邻居的交互的断言.

所以,主要的区别是:

  • 对存根设置的期望通常是通用的,而对模拟设置的期望可以更"聪明"(例如,在第一次调用时返回此信息,在第二次调用时返回此信息等).
  • 存根主要用于设置SUT的间接输入,而模拟可用于测试SUT的间接输入和间接输出.

总而言之,同时也试图驱散福勒的文章标题中的混乱:模拟是存根,但它们不仅仅是存根.


Dee*_*her 7

存根

存根是用于假具有方法的对象的预编程的行为。您可能希望使用此方法而不是现有方法以避免不必要的副作用(例如,存根可能会进行虚假的 fetch 调用,该调用会返回预编程的响应,而实际上并未向服务器发出请求)。

嘲笑

模拟是用于假已对象的方法预编程行为以及预编程的期望。如果这些期望没有得到满足,那么模拟将导致测试失败(例如,模拟可以进行一个假的 fetch 调用,该调用返回一个预编程的响应,而不实际向服务器发出请求,该请求会期望例如第一个参数http://localhost:3008/不是测试将失败。)

区别

与模拟不同,存根没有可能会导致测试失败的预编程期望。


Kas*_*per 6

那里有很多有效的答案,但我认为值得一提的是这种形式的鲍勃叔叔:https : //8thlight.com/blog/uncle-bob/2014/05/14/TheLittleMocker.html

有史以来最好的例子解释!


A.I*_*A.I 5

我偶然看到了UncleBob的《小小嘲笑者》这个有趣的文章。它以一种非常容易理解的方式解释了所有术语,因此对初学者很有用。马丁·福尔斯(Martin Fowlers)的文章特别是对像我这样的初学者来说,是一本精读的文章。


Pre*_*raj 5

他使用的通用术语是测试替身(想想特技替身)。测试替身是任何为了测试目的而替换生产对象的情况的通用术语。Gerard 列出了各种类型的 double:

  • 虚拟对象被传递但从未实际使用。通常它们只是用来填充参数列表。
  • 对象实际上有工作实现,但通常采取一些捷径使它们不适合生产(InMemoryTestDatabase 是一个很好的例子)。
  • 存根为测试期间拨打的电话提供预设答案,通常根本不响应测试编程之外的任何内容。
  • 间谍是存根,它还会根据它们的调用方式记录一些信息。其中一种形式可能是电子邮件服务,它记录发送了多少消息(也称为Partial Mock)。
  • Mocks预先编程了期望,这些期望形成了他们期望接收的调用的规范。如果他们收到一个他们不期望的呼叫并且在验证期间被检查以确保他们得到了他们期望的所有呼叫,他们可以抛出异常。

来源


dav*_*xxx 5

模拟既是技术对象又是功能对象。

模拟是技术性的。由于字节码生成,它确实是由一个模拟库(EasyMock、JMockit 和最近的 Mockito 以这些而闻名)创建的。
模拟实现的生成方式使我们可以在调用方法时对其进行检测以返回特定值,但也可以进行其他一些操作,例如验证是否使用某些特定参数(严格检查)或任何参数调用了模拟方法(没有严格的检查)。

实例化一个模拟:

@Mock Foo fooMock
Run Code Online (Sandbox Code Playgroud)

记录行为:

when(fooMock.hello()).thenReturn("hello you!");
Run Code Online (Sandbox Code Playgroud)

验证调用:

verify(fooMock).hello()
Run Code Online (Sandbox Code Playgroud)

这些显然不是实例化/覆盖 Foo 类/行为的自然方式。这就是我提到技术方面的原因。

但是模拟也是功能性的,因为它是我们需要与 SUT 隔离的类的实例。有了记录的行为,我们就可以像使用存根一样在 SUT 中使用它。


存根只是一个功能对象:它是我们需要与 SUT 隔离的类的实例,仅此而已。这意味着必须明确定义单元测试期间所需的存根类和所有行为装置。
例如,存根hello()需要对类进行子Foo类化(或实现它的接口)并覆盖hello()

public class HelloStub extends Hello{    
  public String hello { 
      return "hello you!"; 
  }
}
Run Code Online (Sandbox Code Playgroud)

如果另一个测试场景需要另一个值返回,我们可能需要定义一个通用的方法来设置返回值:

public class HelloStub extends Hello{    
  public HelloStub(String helloReturn){
       this.helloReturn = helloReturn;
  }
  public String hello { 
      return helloReturn; 
  }
}
Run Code Online (Sandbox Code Playgroud)

其他场景:如果我有一个副作用方法(无返回)并且我会检查该方法是否被调用,我可能应该在存根类中添加一个布尔值或一个计数器来计算调用该方法的次数。


结论

存根通常需要大量开销/代码来编写单元测试。由于提供开箱即用的录制/验证功能,模拟可以防止什么。
这就是为什么现在,随着优秀模拟库的出现,存根方法很少在实践中使用。


关于 Martin Fowler 文章:当我使用模拟并且避免存根时,我不认为自己是一个“模拟者”程序员。
但是我在真正需要时使用模拟(烦人的依赖项),并且当我测试具有依赖项的类时,我更喜欢测试切片和小型集成测试,而模拟将是一种开销。


ahm*_*l88 5

加上有用的答案,使用 Mocks 比 Subs强大的一点之一

如果合作者 [主要代码依赖于它]不在我们的控制之下(例如来自第三方库),
在这种情况下,stub 比 mock 更难编写