模拟对象的目的是什么?

age*_*217 157 unit-testing mocking

我是单元测试的新手,我不断听到很多东西被抛出的"模拟对象".通俗地说,有人可以解释什么是模拟对象,以及在编写单元测试时它们通常用于什么?

Ber*_*t F 341

既然你说你是单位测试的新手,并且以"外行人的术语"要求模拟对象,我会尝试一个外行的例子.

单元测试

想象一下这个系统的单元测试:

cook <- waiter <- customer
Run Code Online (Sandbox Code Playgroud)

通常很容易设想测试低级组件,如cook:

cook <- test driver
Run Code Online (Sandbox Code Playgroud)

测试驾驶员只需订购不同的菜肴,并验证厨师为每个订单返回正确的菜肴.

更难以测试利用其他组件行为的中间组件,如服务员.一个天真的测试人员可能会像测试cook组件一样测试服务器组件:

cook <- waiter <- test driver
Run Code Online (Sandbox Code Playgroud)

测试司机会订购不同的菜肴,并确保服务员返回正确的菜肴.不幸的是,这意味着服务员组件的这种测试可能取决于烹饪组件的正确行为.如果烹饪组件具有任何测试不友好的特征,例如非确定性行为(菜单包括厨师的惊喜作为菜肴),许多依赖性(厨师不会在没有他的全体员工的情况下烹饪),或许多资源(有些菜需要昂贵的食材或需要一个小时的时间来烹饪).

由于这是一个服务员测试,理想情况下,我们只想测试服务员,而不是厨师.具体来说,我们希望确保服务员正确地将客户的订单传达给厨师,并正确地将厨师的食物送到客户手中.

单元测试意味着独立测试单元,因此更好的方法是使用Fowler称为测试双打(假人,存根,假货,模拟)来隔离被测组件(服务员).

    -----------------------
   |                       |
   v                       |
test cook <- waiter <- test driver
Run Code Online (Sandbox Code Playgroud)

在这里,测试厨师与测试驾驶员"合作".理想情况下,被测系统的设计使得测试厨师可以很容易地替换(注入)以与服务员一起工作而无需改变生产代码(例如,不改变服务员代码).

模拟物体

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

  • 假厨师 - 假装自己做饭的人用冷冻晚餐和微波炉,
  • 一个存根厨师 - 一个热狗供应商,无论你订购什么,总是给你热狗,或者
  • 一个模拟厨师 - 一个卧底警察跟随一个假装在刺痛操作中做饭的剧本.

有关fakes vs stubs与mocks vs dummies的更多细节,请参阅Fowler的文章,但就目前而言,让我们专注于模拟厨师.

    -----------------------
   |                       |
   v                       |
mock cook <- waiter <- test driver
Run Code Online (Sandbox Code Playgroud)

服务员组件的单元测试的一个重要部分集中在服务员如何与烹饪组件交互.基于模拟的方法侧重于完全指定正确的交互是什么,并检测何时出错.

模拟对象事先知道在测试期间应该发生什么(例如,将调用哪个方法调用等),并且模拟对象知道它应该如何反应(例如,提供什么返回值).模拟将指示实际发生的事情是否与应该发生的事情不同.可以从头开始为每个测试用例创建自定义模拟对象,以执行该测试用例的预期行为,但是模拟框架努力允许在测试用例中直接且容易地指示这样的行为规范.

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

测试司机模拟厨师:期待一个热狗订单,并给他这个假的热狗作为回应

测试驱动程序(冒充客户)服务员:我想一个热狗请
服务员模拟厨师:1个热狗请
嘲笑厨师服务员:为了达到:1个热狗准备(给假热狗到服务员)
服务员,以试车手:这是你的热狗(给测试司机的假热狗)

测试车手:测试成功了!

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

测试司机模拟厨师:期待一个热狗订单,并给他这个假的热狗作为回应

测试驱动程序(冒充客户)服务员:我想一个热狗请
服务员模拟厨师:1个汉堡包请
嘲笑厨师停止测试:有人告诉我,期待一个热狗订购!

测试驾驶员注意到问题:测试失败! - 服务员改变了订单

要么

测试司机模拟厨师:期待一个热狗订单,并给他这个假的热狗作为回应

测试驱动程序(冒充客户)服务员:我想一个热狗请
服务员模拟厨师:1个热狗请
嘲笑厨师服务员:为了达到:1个热狗准备(给假热狗到服务员)
服务员,以试车手:这是你的炸薯条(从其他订单中给炸薯条测试司机)

测试司机注意到意外炸薯条:测试失败!服务员给了错误的菜

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

另请注意,这是一个非常简单的示例,并且模拟框架允许从组件的一些非常复杂的预期行为规范来支持全面测试.有关更多信息的模拟对象和模拟框架有很多材料.

  • 这是一个很好的解释,但是你不是在某种程度上测试服务员的实现吗?在你的情况下,它可能没关系,因为你正在检查它是否使用了正确的API,但是如果有不同的方法可以做什么,服务员可能会选择其中一个?我认为单元测试的重点是测试API,而不是实现.(当我读到关于嘲笑时,这是我总是发现自己会问的一个问题.) (12认同)
  • 谢谢.我不能说我们是否在没有看到(或定义)服务员的规范的情况下测试"实现".您可以假设服务员可以自己做饭或在街上填写订单,但我认为服务员的规格包括使用预定的厨师 - 毕竟,生产厨师是一位昂贵的美食厨师,我们'我更喜欢我们的服务员使用他.没有那个规范,我想我必须得出结论你是对的 - 服务员可以填写订单,但它想要"正确".OTOH,没有规范,测试没有意义.[续...] (7认同)
  • 从来没有,你做了一个伟大的观点,导致白盒与黑盒单元测试的奇妙主题.我不认为业界一致认为单元测试必须是黑盒而不是白盒("测试API,而不是实现").我认为最好的单元测试可能需要两者结合才能平衡测试脆弱性与代码覆盖率和测试用例完整性. (7认同)

Rob*_*vey 27

模拟对象是替换真实对象的对象.在面向对象的编程中,模拟对象是模拟对象,它们以受控方式模仿真实对象的行为.

计算机程序员通常创建模拟对象来测试某些其他对象的行为,这与汽车设计师使用碰撞测试假人来模拟人类在车辆撞击中的动态行为的方式非常相似.

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

模拟对象允许您设置测试场景,而无需承担大型,笨拙的资源,如数据库.您可以使用单元测试中的模拟对象来模拟数据库,而不是调用数据库进行测试.这使您免于必须设置和拆除真实数据库的负担,只是为了测试您班级中的单个方法.

"Mock"这个词有时会错误地与"Stub"互换使用.这里描述了两个词之间的差异. 本质上,mock是一个存根对象,它还包括对于被测对象/方法的正确行为的期望(即"断言").

例如:

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());
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,warehousemailer模拟对象都使用预期结果进行编程.

  • 您给出的定义与"存根对象"的差别不大,因此不能解释模拟对象是什么. (2认同)

Dan*_*cco 15

模拟对象是模拟真实对象行为的模拟对象.通常,在以下情况下编写模拟对象:

  • 真实对象太复杂,无法将其合并到单元测试中(例如,网络通信,您可以拥有模拟对象的模拟对象)
  • 您的对象的结果是不确定的
  • 真实对象尚不可用


Pet*_*ans 12

Mock对象是一种Test Double.您正在使用mockobjects来测试和验证被测试类与其他类的协议/交互.

通常,您会有"编程"或"记录"期望:您期望您的类对底层对象执行的方法调用.

比方说,我们正在测试一种服务方法来更新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());
Run Code Online (Sandbox Code Playgroud)

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


Ada*_*tek 11

我强烈推荐一篇由Martin Fowler撰写精彩文章,解释嘲笑究竟是什么以及它们与存根的区别.

  • 不完全适合初学者,是吗? (9认同)

Dav*_*all 9

当单元测试计算机程序的某些部分时,理想情况下,您只想测试该特定部分的行为.

例如,从一个虚构的程序片段中查看下面的伪代码,该程序使用另一个程序来调用print:

If theUserIsFred then
    Call Printer(HelloFred)
Else
   Call Printer(YouAreNotFred)
End
Run Code Online (Sandbox Code Playgroud)

如果您正在测试它,您将主要想要测试用户是否为Fred的部分.你真的不想测试Printer一部分事情.那将是另一个考验.

就是Mock对象的用武之地.他们假装是其他类型的东西.在这种情况下,你会使用一个Mock,Printer这样它就像一个真正的打印机,但不会做一些不方便的事情,如打印.


您可以使用的其他几种类型的伪装对象不是Mocks.Mocks Mocks的主要功能在于它们可以配置行为和期望.

期望允许您的Mock在错误使用时引发错误.因此,在上面的示例中,您可能希望确保在"user is Fred"测试用例中使用HelloFred调用打印机.如果没有发生,你的模拟可以警告你.

模拟中的行为意味着,例如,您的代码执行的操作类似于:

If Call Printer(HelloFred) Returned SaidHello Then
    Do Something
End
Run Code Online (Sandbox Code Playgroud)

现在,您要测试代码在调用Printer时所执行的操作并返回SaidHello,因此您可以设置Mock以在使用HelloFred调用时返回SaidHello.

围绕这个的一个很好的资源是Martin Fowlers post Mocks Are Not Stubs


Igo*_*aka 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));
}
Run Code Online (Sandbox Code Playgroud)

在上面的例子中,我使用Moq来演示存根和模拟.Moq对两者使用相同的类 - Mock<T>这使得它有点混乱.无论如何,在运行时,如果output.Write没有使用数据调用,测试将失败parameter,而调用input.Read()失败则不会失败.