模拟对象的目的是啥?
Posted
技术标签:
【中文标题】模拟对象的目的是啥?【英文标题】:What is the purpose of mock objects?模拟对象的目的是什么? 【发布时间】:2011-04-07 01:15:28 【问题描述】:我是单元测试的新手,经常听到“模拟对象”这个词。通俗地说,有人可以解释什么是模拟对象,以及在编写单元测试时它们通常用于什么?
【问题讨论】:
它们是一种大规模过度设计事物的工具,具有您不需要解决手头问题的灵活性。 What is Mocking?的可能重复 【参考方案1】:既然你说你是单元测试的新手,并且用“外行的术语”要求模拟对象,我将尝试一个外行的例子。
单元测试
想象一下这个系统的单元测试:
cook <- waiter <- customer
通常很容易设想测试像cook
这样的低级组件:
cook <- test driver
测试司机只需点不同的菜,然后验证厨师为每个订单返回正确的菜。
更难测试一个中间组件,比如服务员,它利用了其他组件的行为。一个天真的测试人员可能会像测试厨师组件一样测试服务员组件:
cook <- waiter <- test driver
试驾员会点不同的菜,并确保服务员返回正确的菜。不幸的是,这意味着对服务员组件的测试可能依赖于厨师组件的正确行为。如果 cook 组件具有任何不利于测试的特性,例如不确定性行为(菜单包括厨师作为一道菜的惊喜)、大量依赖项(厨师不会在没有他的全部员工的情况下做饭)或大量资源(有些菜肴需要昂贵的食材或需要一个小时才能烹饪)。
由于这是一个服务员测试,理想情况下,我们只想测试服务员,而不是厨师。具体来说,我们要确保服务员正确地将客户的订单传达给厨师,并将厨师的食物正确地交付给客户。
单元测试意味着独立测试单元,因此更好的方法是使用 Fowler calls test doubles (dummies, stubs, fakes, mocks) 隔离被测组件(服务员)。
-----------------------
| |
v |
test cook <- waiter <- test driver
在这里,测试厨师与测试驾驶员“串通一气”。理想情况下,被测系统的设计使得测试厨师可以轻松替代 (injected) 与服务员一起工作,而无需更改生产代码(例如,不更改服务员代码)。
模拟对象
现在,测试厨师(测试替身)可以通过不同的方式实现:
假厨师 - 使用冷冻晚餐和微波炉冒充厨师的人, 存根厨师 - 热狗供应商,无论您点什么,热狗都会给您,或者 模拟厨师 - 一名卧底警察按照剧本假装自己是一名厨师进行刺痛行动。见Fowler's article for the more specifics about fakes vs stubs vs mocks vs dummies,但现在,让我们专注于模拟厨师。
-----------------------
| |
v |
mock cook <- waiter <- test driver
对服务员组件进行单元测试的很大一部分集中在服务员与厨师组件的交互方式上。基于模拟的方法侧重于完全指定正确的交互是什么,并检测何时出错。
模拟对象预先知道在测试期间应该发生什么(例如,它的哪些方法调用将被调用等)并且模拟对象知道它应该如何反应(例如提供什么返回值) .模拟将表明真正发生的事情是否与应该发生的事情不同。可以为每个测试用例从头开始创建自定义模拟对象,以执行该测试用例的预期行为,但模拟框架力求让此类行为规范在测试用例中直接清晰、轻松地指示出来。
围绕基于模拟的测试的对话可能如下所示:
测试司机到模拟厨师:期待一个热狗订单并给他这个假热狗作为回应
测试司机(冒充客户)对服务员:我想要一个热狗服务员 strong> 到 模拟厨师:请 1 个热狗模拟厨师 到 服务员:订购up: 1 个热狗准备好了(给服务员假热狗)服务员到测试司机:这是你的热狗(给虚拟热狗来测试司机)
测试驱动程序:测试成功!
但由于我们的服务员是新来的,可能会发生这种情况:
测试司机到模拟厨师:期待一个热狗订单并给他这个假热狗作为回应
测试司机(冒充客户)对服务员:我想要一个热狗服务员 strong> 到 模拟厨师:请 1 个汉堡包模拟厨师 停止测试:我被告知要吃热狗订购!
测试驱动程序指出问题:测试失败! - 服务员改了订单
或
测试司机到模拟厨师:期待一个热狗订单并给他这个假热狗作为回应
测试司机(冒充客户)对服务员:我想要一个热狗服务员 strong> 到 模拟厨师:请 1 个热狗模拟厨师 到 服务员:订购up: 1 个热狗准备好了(给服务员假热狗)服务员到测试司机:这是你的薯条(给来自其他订单的炸薯条来测试驱动程序)
测试驱动程序注意到意外的炸薯条:测试失败!服务员回错菜了
如果没有基于存根的对比示例,可能很难清楚地看到模拟对象和存根之间的区别,但是这个答案已经太长了:-)
还要注意,这是一个非常简单的示例,并且模拟框架允许对组件的预期行为进行一些非常复杂的规范,以支持全面的测试。有大量关于模拟对象和模拟框架的材料以获取更多信息。
【讨论】:
这个解释很好,但是你不是在某种程度上测试waiter的实现吗?在您的情况下,这可能没问题,因为您正在检查它是否使用正确的 API,但是如果有不同的方法可以做到这一点,而服务员可能会选择其中一种呢?我认为单元测试的目的是测试 API,而不是实现。 (当我读到关于嘲笑的文章时,我总是发现自己在问这个问题。) 谢谢。我不能说我们是否在没有看到(或定义)服务员规范的情况下测试“实现”。您可能会认为服务员可以自己做饭或在街上点菜,但我认为服务员的规范包括使用预期的厨师 - 毕竟,生产厨师是一位昂贵的美食厨师,我们更喜欢我们的服务员使用他。如果没有那个规范,我想我必须得出结论你是对的 - 服务员可以按照它想要的方式填写订单,以便“正确”。OTOH,没有规范,测试是没有意义的。[继续...] 无论如何,你提出了一个很好的观点,引出了白盒与黑盒单元测试的奇妙话题。我认为没有行业共识说单元测试必须是黑盒而不是白盒(“测试 API,而不是实现”)。我认为最好的单元测试可能需要将两者结合起来,以平衡测试脆弱性与代码覆盖率和测试用例完整性。 这个答案在我看来不够技术。我想知道当我可以使用真实对象时为什么要使用模拟对象。 很好的解释!!谢谢!! @BertF【参考方案2】:模拟对象是替代真实对象的对象。在面向对象的编程中,模拟对象是以受控方式模仿真实对象行为的模拟对象。
计算机程序员通常会创建一个模拟对象来测试其他对象的行为,就像汽车设计师使用碰撞测试假人来模拟人类在车辆碰撞中的动态行为一样。
http://en.wikipedia.org/wiki/Mock_object
Mock 对象允许您设置测试场景,而无需使用数据库等大型、笨重的资源。您可以在单元测试中使用模拟对象来模拟数据库,而不是调用数据库进行测试。这使您不必为了测试类中的单个方法而必须设置和拆除一个真实的数据库。
“Mock”一词有时会与“Stub”互换使用,这是错误的。这两个词之间的区别是described here. 本质上,模拟是一个存根对象,它还包括对被测对象/方法的正确行为的期望(即“断言”)。
例如:
class OrderInteractionTester...
public void testOrderSendsMailIfUnfilled()
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());
mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
请注意,warehouse
和 mailer
模拟对象已根据预期结果进行编程。
【讨论】:
您给出的定义与“存根对象”有很大不同,因此并不能解释什么是模拟对象。 另一个更正“'Mock'这个词有时错误地与'stub'互换使用”。 @Myst:这两个词的用法不通用;它因作者而异。福勒是这么说的,***的文章也是这么说的。但是,请随时编辑更改并删除您的反对票。 :) 我同意罗伯特的观点:“mock”这个词的用法在整个行业中往往会有所不同,但根据我的经验,没有集定义,除了它通常不是实际正在测试的对象,而是为了方便测试而存在,使用实际对象或它的所有部分将非常不方便且影响不大。【参考方案3】:模拟对象是模仿真实对象行为的模拟对象。通常,如果满足以下条件,您会编写一个模拟对象:
真实对象太复杂,无法将其合并到单元测试中(对于 例如网络通信, 你可以有一个模拟对象 模拟成为另一个同伴) 您的对象的结果是非 确定性 实物尚不可用【讨论】:
【参考方案4】:Mock 对象是Test Double 的一种。您正在使用模拟对象来测试和验证被测类与其他类的协议/交互。
通常,您会进行某种“程序”或“记录”期望:您希望您的类对底层对象执行的方法调用。
假设我们正在测试一个服务方法来更新 Widget 中的字段。在您的架构中,有一个 WidgetDAO 处理数据库。与数据库对话很慢,设置和清理很复杂,所以我们将模拟 WidgetDao。
让我们考虑一下服务必须做什么:它应该从数据库中获取一个 Widget,对其进行处理并再次保存它。
所以在带有伪模拟库的伪语言中,我们会有类似的东西:
Widget sampleWidget = new Widget();
WidgetDao mock = createMock(WidgetDao.class);
WidgetService svc = new WidgetService(mock);
// record expected calls on the dao
expect(mock.getById(id)).andReturn(sampleWidget);
expect(mock.save(sampleWidget);
// turn the dao in replay mode
replay(mock);
svc.updateWidgetPrice(id,newPrice);
verify(mock); // verify the expected calls were made
assertEquals(newPrice,sampleWidget.getPrice());
通过这种方式,我们可以轻松地测试依赖于其他类的类的驱动开发。
【讨论】:
【参考方案5】:我强烈推荐 great article by Martin Fowler 解释什么是模拟以及它们与存根有何不同。
【讨论】:
对初学者不太友好,是吗? @Robert Harvey:也许,无论如何,很高兴看到它有助于澄清你的答案:) Martin Fowler 的文章以 RFC 的方式编写:干而冷。【参考方案6】:在对计算机程序的某些部分进行单元测试时,您最好只测试该特定部分的行为。
例如,看看下面的伪代码来自一个虚构的程序片段,它使用另一个程序调用 print 一些东西:
If theUserIsFred then
Call Printer(HelloFred)
Else
Call Printer(YouAreNotFred)
End
如果您要对此进行测试,您主要想测试查看用户是否为 Fred 的部分。您真的不想测试 Printer
的一部分。那将是另一个测试。
这是 Mock 对象的用武之地。它们伪装成其他类型的东西。在这种情况下,您将使用 Mock Printer
,这样它就可以像真正的打印机一样工作,但不会做打印等不方便的事情。
您可以使用其他几种不是 Mocks 的假装对象。制作 Mocks Mocks 的主要特点是它们可以根据行为和期望进行配置。
期望允许您的 Mock 在使用不当时引发错误。因此,在上面的示例中,您可能希望确保在“用户是 Fred”测试用例中使用 HelloFred 调用打印机。如果没有发生这种情况,您的 Mock 可以警告您。
Mocks 中的行为 意味着例如,您的代码执行了以下操作:
If Call Printer(HelloFred) Returned SaidHello Then
Do Something
End
现在您想测试您的代码在调用 Printer 并返回 SaidHello 时执行的操作,因此您可以设置 Mock 在使用 HelloFred 调用时返回 SaidHello。
一个很好的资源是 Martin Fowlers 的帖子Mocks Aren't Stubs
【讨论】:
【参考方案7】:模拟和存根对象是单元测试的关键部分。事实上,它们在确保您测试的是单位,而不是组单位方面大有帮助。
简而言之,您使用存根来打破 SUT(被测系统)对其他对象的依赖,并通过模拟来做到这一点并验证 SUT 是否调用了依赖项上的某些方法/属性。这又回到了单元测试的基本原则——测试应该易于阅读、快速且不需要配置,这可能意味着使用所有真实的类。
通常,您的测试中可以有多个存根,但您应该只有一个模拟。这是因为 mock 的目的是验证行为,而您的测试应该只测试一件事。
使用 C# 和 Moq 的简单场景:
public interface IInput
object Read();
public interface IOutput
void Write(object data);
class SUT
IInput input;
IOutput output;
public SUT (IInput input, IOutput output)
this.input = input;
this.output = output;
void ReadAndWrite()
var data = input.Read();
output.Write(data);
[TestMethod]
public void ReadAndWriteShouldWriteSameObjectAsRead()
//we want to verify that SUT writes to the output interface
//input is a stub, since we don't record any expectations
Mock<IInput> input = new Mock<IInput>();
//output is a mock, because we want to verify some behavior on it.
Mock<IOutput> output = new Mock<IOutput>();
var data = new object();
input.Setup(i=>i.Read()).Returns(data);
var sut = new SUT(input.Object, output.Object);
//calling verify on a mock object makes the object a mock, with respect to method being verified.
output.Verify(o=>o.Write(data));
在上面的示例中,我使用 Moq 来演示存根和模拟。 Moq 对两者都使用相同的类 - Mock<T>
,这有点令人困惑。无论如何,在运行时,如果 output.Write
没有以 parameter
的数据调用测试将失败,而调用 input.Read()
失败则不会失败。
【讨论】:
【参考方案8】:作为通过“Mocks Aren't Stubs”链接建议的另一个答案,模拟是一种“测试替身”形式,用于代替真实对象。它们与其他形式的测试替身(例如存根对象)的不同之处在于,其他测试替身提供状态验证(和可选的模拟),而模拟提供行为验证(和可选的模拟)。
使用存根,您可以按任何顺序(甚至重复地)调用存根上的多个方法,并确定存根是否已捕获您想要的值或状态。相比之下,模拟对象期望以特定顺序甚至特定次数调用非常特定的函数。使用模拟对象的测试将被视为“失败”,因为方法以不同的顺序或计数调用 - 即使模拟对象在测试结束时具有正确的状态!
通过这种方式,模拟对象通常被认为与 SUT 代码的耦合比存根对象更紧密。这可能是好事也可能是坏事,这取决于您要验证的内容。
【讨论】:
【参考方案9】:使用模拟对象的部分意义在于它们不必真正按照规范实现。他们只能给出虚假的回应。例如。如果您必须实现组件 A 和 B,并且两者都相互“调用”(交互),那么在实现 B 之前您不能测试 A,反之亦然。在测试驱动开发中,这是一个问题。因此,您为 A 和 B 创建了模拟(“虚拟”)对象,这非常简单,但在与之交互时它们会给出一些类型的响应。这样,您就可以使用 B 的模拟对象来实现和测试 A。
【讨论】:
【参考方案10】:对于 php 和 phpunit 在 phpunit 文档中有很好的解释。看这里 phpunit documentation
简单来说,模拟对象只是原始对象的虚拟对象,并返回它的返回值,这个返回值可以在测试类中使用
【讨论】:
【参考方案11】:这是单元测试的主要观点之一。是的,您正在尝试测试您的单个代码单元,并且您的测试结果不应与其他 bean 或对象行为相关。所以你应该使用 Mock 对象和一些简化的相应响应来模拟它们。
【讨论】:
以上是关于模拟对象的目的是啥?的主要内容,如果未能解决你的问题,请参考以下文章
当我尝试包含对模拟对象的期望时,此 SecurityException 的原因是啥?
在 Angular 6 中使用 rxjs 可观察对象的目的是啥?与 async/await 相比,rxjs 的优势是啥? [复制]