模拟和存根之间有什么区别?
我已经阅读了各种关于嘲笑testing的文章,包括Martin Fowler的Mocks不是Stubs ,但是还是不了解其中的差别。
存根
我相信最大的区别就是你已经用预定的行为写了一个存根。 所以你会有一个类来实现你为了testing目的而伪装的依赖(抽象类或接口),而这些方法只会被设置为响应。 他们不会做任何事情,你可能已经在testing之外为它编写了代码。
嘲笑
模拟是作为testing的一部分,你必须设置你的期望。 模拟没有以预定的方式设置,所以你有代码在你的testing中。 嘲笑是在运行时确定的,因为设置期望值的代码在执行任何操作之前必须运行。
区别
使用mock编写的testing通常按照initialize -> set expectations -> exercise -> verify
模式进行testing。 而预先写好的存根会按照initialize -> exercise -> verify
。
相似
两者的目的是消除testing一个类或函数的所有依赖关系,所以你的testing更集中,更简单,他们正在试图certificate。
前言
有几个对象的定义,是不是真实的。 总称是testing双 。 这个术语包括: 假的 , 假的 , 存根 , 模拟 。
参考
根据Martin Fowler的文章 :
- 虚拟对象被传递但从未实际使用。 通常他们只是用来填充参数列表。
- 虚假的对象实际上有工作的实现,但通常采取一些捷径,使他们不适合生产(内存数据库是一个很好的例子)。
- 存根提供了在testing过程中进行调用的jar装答案,通常对testing以外的任何事情都没有响应。 存根还可以logging关于呼叫的信息,例如logging其“发送”消息的电子邮件网关存根,或者可能仅logging“发送”的消息的数量。
- 嘲笑是我们在这里谈论的东西:预先编程有预期的对象,这些预期构成了预期接收的呼叫的规格。
样式
Mocks vs Stubs =行为testing与状态testing
原理
根据每个testing只testing一件东西的原则,在一次testing中可能有几个存根,但通常只有一个模拟。
生命周期
testing与存根的生命周期:
- 设置 – 准备正在testing的对象及其存根协作者。
- 练习 – testingfunction。
- validation状态 – 使用断言检查对象的状态。
- 拆解 – 清理资源。
testing生命周期与嘲笑:
- 设置数据 – 准备正在testing的对象。
- 设置期望 – 在主对象正在使用的模拟中准备期望值。
- 练习 – testingfunction。
- validation期望 – validation在模拟中调用了正确的方法。
- validation状态 – 使用断言检查对象的状态。
- 拆解 – 清理资源。
概要
嘲笑和存根testing都给出了这个问题的答案: 结果是什么?
嘲笑testing也感兴趣: 结果如何实现?
存根是简单的假物体。 这只是确保testing运行顺利。
模拟是更聪明的存根。 您validation您的testing通过它。
在codeschool.com课程中, “僵尸testing” ( Rails Testing for Zombies)中 ,他们给出了这些术语的定义:
存根
用代码返回指定结果的方法。
嘲笑
一个断言的方法被调用的断言。
正如肖恩·哥本哈根在他的回答中所描述的那样,不同之处在于嘲笑确定了期望值(即断言,关于是否或如何被调用)。
下面是对每一个跟随现实世界样本的描述。
-
虚拟 – 只是虚假的价值观,以满足
API
。示例 :如果您正在testing一个类的方法,该类需要构造函数中的许多必需参数,而这些参数对您的testing没有任何影响 ,则可以创build虚拟对象来创build类的新实例。
-
假 – 创build可能依赖某些外部基础结构的类的testing实现。 (您的unit testing实际上不会与外部基础架构进行交互,这是很好的做法。)
示例 :创build用于访问数据库的假实现,将其replace
in-memory
集合。 -
存根 – 覆盖方法返回硬编码值,也称为
state-based
。示例 :您的testing类取决于
Calculate()
方法需要5分钟才能完成。 而不是等待5分钟,你可以用存根返回硬编码的值replace它的真实实现。 只占了一小部分时间。 -
模拟 – 与
Stub
非常相似,但是interaction-based
而不是基于状态的。 这意味着你不希望Mock
返回一些值,而是假定进行方法调用的具体顺序。示例:您正在testing用户注册类。 调用
Save
,它应该调用SendConfirmationEmail
。
Stubs
和Mock
实际上是Mock
子types,它们都将真正的实现与testing实现交换,但出于不同的具体原因。
存根不会让你的testing失败,模拟可以。
我认为关于这个问题的最简单和最清晰的答案是由Roy Osherove在他的“ unit testing的艺术” (第85页)
告诉我们正在处理一个存根的最简单的方法是注意存根永远不会失败的testing。 断言testing用法总是针对被testing的类。
另一方面,testing将使用模拟对象来validationtesting是否失败。 […]
再次,模拟对象是我们用来查看testing是否失败的对象。
这意味着如果你对这个假冒做出断言,这意味着你正在使用假冒作为一个模仿,假如你使用这个假冒来运行testing而没有对它进行断言,那么你正在使用这个假冒作为一个存根。
我认为他们之间最重要的区别就是他们的意图。
让我试着解释它为什么存根与为什么模拟
假设我正在为我的mac twitter客户端的公共时间线控制器编写testing代码
这里是testing示例代码
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
- STUB:twitter API的networking连接速度很慢,这使我的testing变慢。 我知道它会返回时间线,所以我做了一个模拟HTTP twitter API的存根,所以我的testing将运行得非常快,即使我离线也可以运行testing。
- MOCK:我还没有写我的任何UI方法,我不知道我需要为我的UI对象写什么方法。 我希望通过编写testing代码知道我的控制器将如何与我的ui对象进行协作。
通过编写模拟,通过validation满足期望来发现对象的协作关系,而存根则只模拟对象的行为。
我build议阅读这篇文章,如果你想了解更多关于模拟: http : //jmock.org/oopsla2004.pdf
模拟只是testing行为,确保某些方法被调用。 存根是一个特定对象的可testing版本(每说)。
你是什么意思苹果的方式?
如果你把它比作debugging:
存根就像确保一个方法返回正确的值
模拟就像实际进入方法 ,并确保在返回正确的值之前,内部的一切是正确的。
要非常清楚和实用:
存根(Stub):实现类/对象的方法的类或对象被伪造,并且总是返回你想要的。
JavaScript中的示例:
var Stub = { method_a: function(param_a, param_b){ return 'This is an static result'; } }
Mock:和stub一样,但是它增加了一些逻辑,当一个方法被调用的时候“validation”,所以你可以确定一些实现正在调用这个方法。
正如@mLevan所说,想象一下你正在testing一个用户注册类。 调用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!'); } }
假的是一个通用的术语,可以用来描述一个存根或一个模拟对象(手写或其他),因为它们都看起来像真实的对象。
无论是假货还是假货,都取决于当前testing中的使用方式。 如果它用于检查交互(断言),它是一个模拟对象。 否则,这是一个存根。
假货确保testing顺利进行。 这意味着未来testing的读者将会明白虚拟对象的行为,而不需要读取其源代码(不需要依靠外部资源)。
testing运行顺利意味着什么?
例如在下面的代码:
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"); } } }
你想testingmailService.SendEMail()方法,要做到这一点,你需要在你的test方法中模拟一个Exception,所以你只需要创build一个Fake Stub的errorService类来模拟那个结果,那么你的testing代码就能够testingmailService.SendEMail()方法。 如您所见,您需要模拟来自另一个External Dependency ErrorService类的结果。
我喜欢Roy Osherove提出的解释[video链接] 。
每一个创build的类或对象都是一个假的。 这是一个模拟,如果你确认电话反对它。 否则它是一个存根。
阅读以上所有的解释,让我试着浓缩:
- 存根 :可以让testing运行的一段代码,但不关心它会发生什么。
- 模拟 :一个虚拟的代码片段,你作为testing的一部分正确调用VERIFY。
这张幻灯片解释了主要的差异非常好。
*华盛顿大学CSE 403第16讲(幻灯片由“Marty Stepp”创作)
从模拟angular色,而不是对象 ,由jMock的开发者:
存根是生产代码的虚拟实现,返回预设的结果。 模拟对象作为存根,但也包括断言来衡量目标对象与其邻居的相互作用。
所以主要的区别是:
- 在存根上设置的期望通常是通用的,而在模拟上设置的期望可以更“聪明”(例如,在第一次调用时返回,第二次返回)。
- 存根主要用于设置SUT的间接input ,而模拟可用于testingSUT的间接input和间接输出。
总而言之,同时也试图分散福勒的文章标题中的混淆: 嘲笑是存根,但它们不仅是存根(stub) 。
我遇到了UncleBob The Little Mocker这篇有趣的文章。 它以一种非常容易理解的方式解释了所有的术语,所以对初学者很有用。 Martin Fowlers的文章对于像我这样的初学者来说尤其难以阅读。
存根帮助我们运行testing。 怎么样? 它给出了有助于运行testing的值。 这些值本身并不是真实的,我们创build这些值只是为了运行testing。 例如,我们创build一个HashMap来为我们提供类似于数据库表中的值的值。 所以不是直接与数据库交互,而是与Hashmap进行交互。
模拟是一个运行testing的假对象。 我们把断言。
请参阅下面的示例使用C#和Moq框架模拟与存根(stub)。 Moq对Stub没有特殊的关键字,但是你也可以使用Mock对象创build存根。
namespace UnitTestProject2 { using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; [TestClass] public class UnitTest1 { /// <summary> /// Test using Mock to Verify that GetNameWithPrefix method calls Repository GetName method "once" when Id is greater than Zero /// </summary> [TestMethod] public void GetNameWithPrefix_IdIsTwelve_GetNameCalledOnce() { // Arrange var mockEntityRepository = new Mock<IEntityRepository>(); mockEntityRepository.Setup(m => m.GetName(It.IsAny<int>())); var entity = new EntityClass(mockEntityRepository.Object); // Act var name = entity.GetNameWithPrefix(12); // Assert mockEntityRepository.Verify(m => m.GetName(It.IsAny<int>()), Times.Once); } /// <summary> /// Test using Mock to Verify that GetNameWithPrefix method doesn't call Repository GetName method when Id is Zero /// </summary> [TestMethod] public void GetNameWithPrefix_IdIsZero_GetNameNeverCalled() { // Arrange var mockEntityRepository = new Mock<IEntityRepository>(); mockEntityRepository.Setup(m => m.GetName(It.IsAny<int>())); var entity = new EntityClass(mockEntityRepository.Object); // Act var name = entity.GetNameWithPrefix(0); // Assert mockEntityRepository.Verify(m => m.GetName(It.IsAny<int>()), Times.Never); } /// <summary> /// Test using Stub to Verify that GetNameWithPrefix method returns Name with a Prefix /// </summary> [TestMethod] public void GetNameWithPrefix_IdIsTwelve_ReturnsNameWithPrefix() { // Arrange var stubEntityRepository = new Mock<IEntityRepository>(); stubEntityRepository.Setup(m => m.GetName(It.IsAny<int>())) .Returns("Stub"); const string EXPECTED_NAME_WITH_PREFIX = "Mr. Stub"; var entity = new EntityClass(stubEntityRepository.Object); // Act var name = entity.GetNameWithPrefix(12); // Assert Assert.AreEqual(EXPECTED_NAME_WITH_PREFIX, name); } } public class EntityClass { private IEntityRepository _entityRepository; public EntityClass(IEntityRepository entityRepository) { this._entityRepository = entityRepository; } public string Name { get; set; } public string GetNameWithPrefix(int id) { string name = string.Empty; if (id > 0) { name = this._entityRepository.GetName(id); } return "Mr. " + name; } } public interface IEntityRepository { string GetName(int id); } public class EntityRepository:IEntityRepository { public string GetName(int id) { // Code to connect to DB and get name based on Id return "NameFromDb"; } } }
我在回答中使用了Python示例来说明差异。
存根 – 存根是一种软件开发技术,用于在开发生命周期的早期阶段实现类的方法。 它们通常用作占位符来实现一个已知的接口,其中接口已经定型或已知,但实现尚未知道或最终完成。 你从存根开始,仅仅意味着你只写下一个函数的定义,留下实际的代码以备后用。 优点是你不会忘记方法,你可以继续考虑你的devise,而在代码中看到它。 你也可以让你的存根返回一个静态的响应,以便你的代码的其他部分可以立即使用这个响应。 存根对象提供了一个有效的响应,但是无论你传入什么input都是静态的,你总会得到相同的响应:
class Foo(object): def bar1(self): pass def bar2(self): #or ... raise NotImplementedError def bar3(self): #or return dummy data return "Dummy Data"
模拟对象用于模拟testing用例,它们validation在这些对象上调用某些方法。 模拟对象是以受控方式模拟真实对象行为的模拟对象。 您通常创build一个模拟对象来testing其他对象的行为。 嘲笑让我们模拟unit testing不可用或太笨重的资源。
mymodule.py:
import os import os.path def rm(filename): if os.path.isfile(filename): os.remove(filename)
test.py:
from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os') def test_rm(self, mock_os): rm("any path") # test that rm called os.remove with the right parameters mock_os.remove.assert_called_with("any path") if __name__ == '__main__': unittest.main()
这是一个非常基本的例子,它运行rm并声明它所调用的参数。 你可以使用模拟对象而不仅仅是这里所示的函数,而且你也可以返回一个值,所以模拟对象可以被用来代替testing的存根。
关于unittest.mock的更多信息,请注意python 2.x mock不包含在unittest中,而是一个可以通过pip(pip install mock)下载的可下载模块。
我还读过Roy Osherove的“unit testing的艺术”,我认为如果用Python和Python编写一个类似的书,这将是一件好事。 如果有人知道这样的书,请分享。 干杯:)
存根是为了testing而构build的假对象。 模拟是一个存根,logging预期的呼叫是否有效发生。
存根是一个空的函数,用于避免在testing过程中出现未处理的exception:
function foo(){}
模拟是一个人造函数,用于在testing过程中避免操作系统,环境或硬件依赖:
function foo(bar){ window = this; return window.toString(bar); }
在断言和状态方面:
- 嘲笑在事件或状态改变之前被断言
- 存根不被断言,它们在事件之前提供状态以避免从不相关单元执行代码
- 间谍设置像存根,然后在事件或状态改变后断言
- 假货没有被断言,他们运行在硬编码依赖的事件之后,以避免状态
参考
- 极客词汇:模拟
- 极客词汇:存根(stub)
- 极客词汇:间谍
- testing双打:假货,嘲笑和存根
存根是一个实现组件接口的对象,但不是返回组件在调用时返回的内容,而是可以将存根configuration为返回适合testing的值。 使用存根可以testing一个单元是否可以处理来自其协作者的各种返回值。 在unit testing中使用存根而不是真正的协作者可以这样expression:
unit testing – >存根
unit testing – >单元 – >存根
unit testing对单元的结果和状态作出断言
首先unit testing创build存根并configuration其返回值。 然后unit testing创build单元并在其上设置存根。 现在unit testing调用单元,然后调用存根。 最后,unit testing对单元上的方法调用的结果做出断言。
一个模拟 像一个存根,只有它也有可能决定哪些方法在模拟上被调用的方法 。 因此,使用模拟可以testing单元是否可以正确处理各种返回值,并且如果单元正确使用协作者。 例如,你不能看到由dao对象返回的值是否使用Statement或PreparedStatement从数据库中读取数据。 在返回值之前,您也不能看到connection.close()方法是否被调用。 这是可能的嘲笑。 换句话说,模拟可以testing单位与合作者的完整交互。 不只是返回单位使用的协作者方法。 在unit testing中使用模拟可以expression如下:
unit testing – >模拟
unit testing – >单元 – >模拟
unit testing声明单元的结果和状态
unit testing在模拟调用的方法上声明
更多详情>> 在这里
以下是我的理解…
-
如果您在本地创buildtesting对象并用本地服务提供您的本地服务,则使用模拟对象。 这将对您在本地服务中实施的方法进行testing。 它用于validation行为
-
当你从真正的服务提供者那里得到testing数据的时候,虽然从一个testing版本的接口获得一个testing版本的对象,但你正在使用存根,这个存根可以有逻辑接受某些input并给出相应的输出来帮助你执行状态validation…
存根用于您在testing中设置的具有预期返回值的方法。 Mocks在void方法上使用,在Assert中validation它们被调用。
模拟 – 一个模拟拦截对一个方法或函数的调用(或者一组方法和函数,就像模拟类一样)。 这不是该方法或function的替代scheme。 在这个拦截过程中,模拟器可以做任何想做的事情,比如logginginput和输出,决定短路,改变返回值等。
存根 – 存根是一个方法或函数(或一组方法和函数,就像存根类一样)的一个有效的完整工作实现,它与方法,函数或者一组方法和函数具有相同的接口/签名是为了保存。 通常情况下,stubbed的实现只会在unit testing的背景下做一些可以接受的事情,这就意味着它不会在做IO的时候模仿它所存储的东西的行为。
存根和模拟testing的观点:
-
存根是用户以静态方式完成的虚拟实现,即在Stub中编写实现代码。 所以它不能处理服务定义和dynamic条件 ,通常这是在JUnit框架中完成的,而不使用模拟框架。
-
模拟也是虚拟的实现,但是它的实现是通过使用Mockito等Mocking框架以dynamic的方式完成的。 所以我们可以dynamic地处理条件和服务定义,也就是说,可以在运行时从代码dynamic创buildmock。 所以使用模拟我们可以dynamic地实现存根。