模拟对象的目的是什么?

我是unit testing的新手,我不断听到“模拟对象”这个词。 用外行的话来说,有人可以解释一下模拟对象是什么,以及在编写unit testing时通常使用什么对象?

既然你说你是unit testing的新手,并以“外行人”的名义要求模仿对象,那么我会尝试一个外行的例子。

unit testing

想象一下这个系统的unit testing:

cook <- waiter <- customer 

通常很容易设想testingcook这样的低级组件:

 cook <- test driver 

testing驾驶员只需要订购不同的盘子,并validation厨师为每个订单返回正确的盘子。

它更难testing一个像服务员那样利用其他组件行为的中间组件。 一个天真的testing者可能会像testingcook组件一样testing服务器组件:

 cook <- waiter <- test driver 

testing司机会下令不同的菜肴,并确保服务员返回正确的菜肴。 不幸的是,这意味着服务员组件的这种testing可能取决于烹饪组件的正确行为。 如果烹饪组件具有任何不友好的特性,如非确定性行为(菜单包括厨师的惊喜作为菜肴),大量依赖性(厨师不会在没有其全体员工的情况下烹饪)或者很多资源(一些菜需要昂贵的配料,或需要一个小时做饭)。

由于这是一个服务员testing,理想情况下,我们只想testing服务员,而不是厨师。 具体而言,我们要确保服务员正确地将客户的订单传达给厨师,并将厨师的食物正确送达客户。

unit testing意味着testing单元是独立的,所以更好的方法是使用福勒所称的testing双打(虚拟,存根,假货,嘲笑)来隔离被测组件(服务员) 。

  ----------------------- | | v | test cook <- waiter <- test driver 

在这里,testing厨师与testing司机“在一起”。 理想情况下,被测系统的devise使得testing厨师可以很容易地replace( 注入 )与服务员一起工作而不改变生产代码(例如,不改变服务员代码)。

模拟对象

现在,testing厨师(testing双)可以实现不同的方式:

  • 一个假冒的厨师 – 一个冒充冷冻晚餐和微波炉的厨师,
  • 一个短线厨师 – 一个热狗供应商总是给你热狗,无论你订购什么,或者
  • 一个模拟厨师 – 一个卧底警察在剧本中假扮成一名厨师的剧本。

请参阅Fowler的文章,了解有关假货与存根(stub)vs mock vs dummies的更多细节 ,但现在让我们关注一个模拟厨师。

  ----------------------- | | v | mock cook <- waiter <- test driver 

unit testing服务员组件的很大一部分集中在服务员如何与厨师组件交互。 基于模拟的方法着重于充分指定正确的交互是什么,并检测何时发生错误。

模拟对象事先知道在testing过程中应该发生什么(例如,哪个方法调用将被调用等),并且模拟对象知道它应该如何反应(例如提供什么样的返回值)。 模拟将表明真正发生的事情是否与预期的不同。 可以从头开始为每个testing用例创build一个自定义的模拟对象,以执行该testing用例的预期行为,但是一个模拟框架试图让这种行为规范直接在testing用例中清晰易懂地指明。

围绕基于模拟testing的对话可能如下所示:

testing司机 嘲笑厨师期待一个热狗订单,并给他这个虚拟的热狗作为回应

testing司机 (冒充顾客)去服务员我想请一个热狗
服务员 嘲笑厨师1热狗请
嘲笑 服务员订购:1热狗准备(给服务员假冒热狗)
服务员 testing司机这是你的热狗(给虚拟的热狗testing司机)

testing司机 :testing成功!

但由于我们的服务员是新的,这是可能发生的事情:

testing司机 嘲笑厨师期待一个热狗订单,并给他这个虚拟的热狗作为回应

testing司机 (冒充顾客)去服务员我想请一个热狗
服务员 嘲笑厨师1个汉堡
嘲笑厨师停止testing: 我被告知期待一个热狗订单!

testing驱动程序注意到问题:testing失败! – 服务员改变了顺序

要么

testing司机 嘲笑厨师期待一个热狗订单,并给他这个虚拟的热狗作为回应

testing司机 (冒充顾客)去服务员我想请一个热狗
服务员 嘲笑厨师1热狗请
嘲笑 服务员订购:1热狗准备(给服务员假冒热狗)
服务员 testing司机这是你的炸薯条(从其他一些命令给司机testing炸薯条)

testing司机注意到意想不到的薯条:testing失败! 服务员给了错误的菜

可能很难清楚地看到模拟对象和存根之间的区别,没有一个对比的基于存根的例子,但是这个答案已经太长了:-)

另外请注意,这是一个相当简单的例子,嘲笑框架允许组件的预期行为的一些非常复杂的规范来支持全面的testing。 有很多关于模拟对象和嘲笑框架的资料可以获取更多信息。

模拟对象是替代真实对象的对象。 在面向对象编程中,模拟对象是模拟对象,以受控方式模拟真实对象的行为。

计算机程序员通常会创build一个模拟对象来testing其他对象的行为,就像汽车devise师使用碰撞testing假人来模拟车辆碰撞中人的dynamic行为一样。

http://en.wikipedia.org/wiki/Mock_object

模拟对象允许您设置testing场景,而不需要承担庞大而笨重的资源,例如数据库。 您可以在unit testing中使用模拟对象来模拟数据库,而不是调用数据库进行testing。 这使您不必担心必须build立和拆除实际的数据库,只能在class级中testing单一的方法。

“模拟”一词有时被错误地与“存根”互换使用。 这两个词之间的区别在这里描述。 从本质上来说,模拟是一个存根对象,它也包含对被测对象/方法正确行为的期望(即“断言”)。

例如:

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

请注意, warehousemailer模拟对象是按预期结果编程的。

模拟对象是一种双testing 。 您正在使用mockobjects来testing和validation与其他类的testing类的协议/交互。

通常情况下,您会对“程序”或“logging”期望types:方法调用您期望您的类对底层对象执行操作。

比方说,我们正在testing一个服务方法来更新Widget中的一个字段。 而在你的架构中有一个WidgetDAO处理数据库。 与数据库交谈很慢,build立和清理之后是复杂的,所以我们将模拟出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()); 

通过这种方式,我们可以轻松地testing依赖于其他类的类的驱动开发。

模拟物体是模仿真实物体行为的模拟物体。 通常你写一个模拟对象,如果:

  • 真正的对象太复杂了,不能将它集成到一个unit testing中(例如,一个networking通信,你可以有一个模拟对象,模拟对方)
  • 你的对象的结果是不确定的
  • 真正的对象尚不可用

我强烈推荐Martin Fowler撰写的一篇很棒的文章,解释嘲笑到底是什么,以及它们与存根的区别。

unit testing计算机程序的某个部分时,理想情况下只需testing该特定部分的行为。

例如,从一个使用另一个程序调用打印内容的虚拟程序块中查看下面的伪代码:

 If theUserIsFred then Call Printer(HelloFred) Else Call Printer(YouAreNotFred) End 

如果你正在testing这个,你主要想testing看看用户是否是Fred的部分。 你真的不想testingPrinter一部分。 那将是另一个考验。

是Mock对象进来的地方。他们假装是其他types的东西。 在这种情况下,您可以使用模拟Printer ,使其像真正的打印机一样工作,但不会进行打印等不方便的事情。


还有其他几种可以使用的不是Mocks的假装对象。 Mocks Mocks的主要之处在于可以configuration行为和期望。

期望让您的模拟错误地使用时发生错误。 所以在上面的例子中,你可以确定打印机是在“user is Fred”testing用例中用HelloFred调用的。 如果这没有发生,你的模拟可以警告你。

在Mocks中的行为意味着,例如,你的代码做了这样的事情:

 If Call Printer(HelloFred) Returned SaidHello Then Do Something End 

现在,您要testing打印机被调用时的代码是什么,并返回SaidHello,因此您可以设置Mock以在使用HelloFred调用时返回SaidHello。

围绕这一点的一个很好的资源是马丁·福勒斯(Martin Fowlers)的post

模拟和存根对象是unit testing的重要组成部分。 事实上,他们还有很长的路要走,以确保你是testing单位 ,而不是单位组。

简而言之,您使用存根来破坏SUT(System Under Test)对其他对象的依赖,并嘲笑这一点, validationSUT在依赖关系上调用某些方法/属性。 这可以追溯到unit testing的基本原则 – testing应该易于阅读,速度快,不需要configuration,而使用所有真实类可能意味着。

一般来说,你可以在testing中有不止一个存根,但是你应该只有一个模拟。 这是因为模拟的目的是validation行为,你的testing应该只testing一件事情。

使用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调用,则testing将失败,而调用input.Read()失败不会使其失败。

作为通过链接到“嘲讽不是存根 ”build议的另一个答案,嘲笑是一种“testing双重”的forms来代替一个真实的对象。 与其他forms的testing双打(如存根对象)不同的是,其他testing双打提供状态validation(和可选的模拟),而模拟提供行为validation(以及可选的模拟)。

对于存根,您可以以任何顺序(甚至是正式)在存根上调用多个方法,并在存根已经捕获到您想要的值或状态时确定成功。 相比之下,模拟对象期望以特定的顺序,甚至特定的次数调用非常特定的function。 使用模拟对象的testing将被视为“失败”,只是因为这些方法是以不同的顺序或计数调用的 – 即使testing结束时模拟对象具有正确的状态!

通过这种方式,模拟对象通常被视为比存根对象更紧密地耦合到SUT代码。 这可能是好事或坏事,这取决于你想要validation的内容。

部分使用模拟对象的一点是,他们不必根据规范真正实施。 他们只能做出虚假的回应。 例如,如果你必须实现组件A和B,并且“相互调用”(交互),那么在B被实现之前,你不能testingA,反之亦然。 在testing驱动开发中,这是一个问题。 所以你为A和B创build了模拟(“虚拟”)对象,非常简单,但是当它们交互时,它们会给出某种响应。 这样,你可以使用模拟对象来实现和testingA

对于php和phpunit,在phpunit文档中有很好的解释。 看到这里的phpunit文档

简单地说,嘲笑对象只是你原来的对象的虚拟对象,并返回它的返回值,这个返回值可以在testing类中使用