模拟对象的目的是什么?

时间:2010-09-01 22:03:07

标签: unit-testing mocking

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

11 个答案:

答案 0 :(得分:323)

答案 1 :(得分:27)

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

计算机程序员通常会创建一个模拟对象来测试某些其他对象的行为,就像汽车设计师使用碰撞测试假人来模拟人类在车辆撞击中的动态行为一样。

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

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

“Mock”这个词有时会错误地与“Stub”互换使用。这两个词之间的差异是described here.本质上,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());
  }
}

请注意,warehousemailer模拟对象已编程为预期结果。

答案 2 :(得分:15)

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

  • 真实对象太复杂,无法将其合并到单元测试中(For 网络通信的例子, 你可以有一个模拟对象 模拟是另一个同行)
  • 您的对象的结果是非结果 确定性
  • 真实对象尚不可用

答案 3 :(得分:12)

模拟对象是一种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());

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

答案 4 :(得分:11)

我强烈建议使用great article by Martin Fowler来解释什么是模拟以及它们与存根的区别。

答案 5 :(得分:9)

在对计算机程序的某些部分进行单元测试时,您最好只测试该特定部分的行为。

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

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

如果您正在测试这个,那么您主要想测试用户是否为Fred的部分。你真的不想测试Printer部分内容。那将是另一个考验。

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


您可以使用的其他几种类型的假装对象不是Mocks。 Mocks模拟的主要原因在于它们可以配置行为和期望。

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

模拟中的行为意味着您的代码例如:

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

现在你要测试你的代码在调用打印机时所做的事情并返回SaidHello,这样你就可以设置Mock在用HelloFred调用时返回SaidHello。

围绕这个的一个很好的资源是Martin Fowlers帖子Mocks Aren't Stubs

答案 6 :(得分:7)

模拟和存根对象是单元测试的关键部分。事实上,他们还有很长的路要走,以确保您正在测试单位,而不是群组

简而言之,您使用存根来破坏SUT(System Under Test)对其他对象和模拟的依赖性,以便验证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()失败则不会失败。

答案 7 :(得分:4)

另一个答案是通过“Mocks Aren't Stubs”的链接建议的,模拟是一种“测试双重”形式,用来代替真实物体。使它们与其他形式的测试双精度(例如存根对象)不同的是,其他测试双精度提供状态验证(以及可选的模拟),而模拟提供行为验证(以及可选的模拟)。

使用存根,您可以按任何顺序(甚至是重复地)在存根上调用多个方法,并在存根捕获了您想要的值或状态时确定成功。相反,模拟对象期望以特定顺序甚至特定次数调用非常特定的函数。使用模拟对象的测试将被视为“失败”,因为方法是以不同的顺序或计数调用的 - 即使模拟对象在测试结束时具有正确的状态!

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

答案 8 :(得分:3)

使用模拟对象的部分原因是它们不必根据规范真正实现。他们可以给出虚假的回应。例如。如果你必须实现组件A和B,并且彼此“调用”(交互),那么在实现B之前你不能测试A,反之亦然。在测试驱动的开发中,这是一个问题。因此,您为A和B创建了模拟(“虚拟”)对象,这非常简单,但是当它们与之交互时,它们会给出一些类型的响应。这样,您就可以使用B的模拟对象来实现和测试A.

答案 9 :(得分:1)

对于php和phpunit在phpunit documentaion中有很好的解释。看这里 phpunit documentation

简单来说,模拟对象只是原始对象的虚拟对象,并返回其返回值,此返回值可用于测试类

答案 10 :(得分:0)

这是单元测试的主要观点之一。是的,您正在尝试测试单个代码单元,并且测试结果与其他bean或对象行为无关。因此,您应该通过使用具有相应简化响应的Mock对象来模拟它们。