单元测试中模拟对象(mock objects)的目的是什么?

我是单元测试的新手,我经常听到“模拟对象”这个词。用门外汉的话说,谁能解释一下什么是模拟对象,以及在编写单元测试时它们通常用于什么?

81329 次浏览

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

计算机程序员通常创建一个模拟对象来测试其他对象的行为,这与汽车设计师使用碰撞测试假人来模拟人在车辆碰撞中的动态行为大致相同。

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

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

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

  • 实际对象太复杂,不能将其合并到单元测试中(对于 例如,网络通信, 你可以有一个模拟对象 模拟是其他对等体)
  • 对象的结果为非 李确定性< / >
  • 真正的对象尚未可用

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

我强烈推荐使用Martin Fowler的好文章来解释mock到底是什么,以及它们与存根有什么不同。

当对计算机程序的某些部分进行单元测试时,理想的情况是只测试该特定部分的行为。

例如,看看下面的伪代码,这些伪代码来自一个使用另一个程序调用print的程序:

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

如果您要测试这个,您将主要测试查看用户是否是Fred的部分。你并不是真的想测试Printer部分的东西。这将是另一个考验。

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


您可以使用其他几种非mock的假装对象。mock的主要特点是可以用行为和期望来配置它们。

预期允许Mock在错误使用时引发错误。因此,在上面的例子中,您可能希望确保在“用户是Fred”测试用例中使用HelloFred调用Printer。如果没有发生,Mock会警告你。

模拟中的行为意味着,例如,你的代码做了如下的事情:

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

现在,您想要测试当调用Printer并返回saidedhello时代码会做什么,因此您可以设置Mock,以便在使用HelloFred调用它时返回saidedhello。

关于这一点的一个很好的资源是Martin fowler的帖子mock不是stub

Mock对象是一种测试双。您正在使用mock对象测试和验证被测类与其他类的协议/交互。

通常你会“编程”或“记录”期望:你期望类对底层对象执行的方法调用。

假设我们正在测试一个服务方法来更新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());

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

通过“mock不是stub”链接给出的另一个答案是,mock是“test double”的一种形式,用于代替真实对象。它们与其他形式的测试替身(如存根对象)的不同之处在于,其他测试替身提供状态验证(和可选模拟),而mock提供行为验证(和可选模拟)。

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

通过这种方式,模拟对象通常被认为比存根对象与SUT代码耦合得更紧密。这可能是一件好事,也可能是一件坏事,这取决于你试图验证什么。

模拟和存根对象是单元测试的关键部分。事实上,为了确保你测试的是单位单元,而不是单元,它们有很长的路要走。

简而言之,你使用存根来打破SUT (System Under Test)对其他对象的依赖关系,并通过mock来验证SUT是否调用了依赖关系上的某些方法/属性。这可以追溯到单元测试的基本原则——测试应该易于阅读、快速且不需要配置,使用所有真实的类可能意味着这一点。

通常,您可以在您的测试中有多个存根,但是您应该只有一个mock。这是因为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来演示存根和mock。Moq对两者使用了相同的类——Mock<T>,这让人有点困惑。无论如何,在运行时,如果没有调用output.Write并且数据为parameter,测试将失败,而调用input.Read()失败将不会失败。

既然您说您是单元测试的新手,并且要求使用“门外汉术语”来模拟对象,那么我将尝试一个门外汉的示例。

单元测试

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

cook <- waiter <- customer

通常很容易想象测试像cook这样的低级组件:

cook <- test driver

测试驱动程序只是点不同的菜,并验证厨师为每个订单返回正确的菜。

测试利用其他组件的行为的中间组件(如服务员)比较困难。一个天真的测试人员可能会像测试厨师组件一样测试服务员组件:

cook <- waiter <- test driver

测试司机会点不同的菜,并确保服务员返回正确的菜。不幸的是,这意味着服务员组件的测试可能依赖于厨师组件的正确行为。如果cook组件具有任何测试不友好的特征,例如不确定性行为(菜单包括厨师的惊喜作为一道菜)、大量依赖(没有他的全体员工,cook就不会做饭)或大量资源(一些菜肴需要昂贵的食材或需要一个小时才能烹饪),则这种依赖性会更糟。

由于这是一个服务员测试,理想情况下,我们只想测试服务员,而不是厨师。具体来说,我们要确保服务员正确地将顾客点的菜传递给厨师,并正确地将厨师的食物传递给顾客。

单元测试意味着独立地测试单元,所以更好的方法是使用Fowler调用测试替身(假人、存根、假人、mock)来隔离被测组件(服务员)。

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

在这里,测试厨师与测试司机“合谋”。理想情况下,被测系统设计成可以很容易地替换测试厨师(注射)与服务员一起工作,而不需要更改生产代码(例如,不更改服务员代码)。

模拟对象

现在,test cook (test double)可以通过不同的方式实现:

  • 假厨师——用冷冻食品和微波炉假装自己是厨师,
  • 一个stub cook -一个热狗小贩,无论你点什么,总是给你热狗,或者
  • 模拟厨师——在一次诱捕行动中,卧底警察按照剧本假装自己是厨师。

参见福勒的文章中有更多关于假货、存根、模仿品和假人的细节,但现在,让我们专注于模拟厨师。

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

对服务员组件进行单元测试的很大一部分集中在服务员如何与厨师组件交互。基于模拟的方法侧重于完全指定正确的交互是什么,并在出错时进行检测。

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

围绕基于模拟的测试的对话可能是这样的:

测试驱动程序模拟做饭: 期待一份热狗订单,然后给他这个假热狗作为回应

测试驱动程序(冒充客户)到服务员: 我想要一个热狗
服务员模拟做饭: 请给我一个热狗
模拟做饭服务员: 点餐:1个热狗准备好(把假热狗递给服务员)
服务生测试驱动程序: 这是你的热狗(把假热狗给测试司机)

__abc0:测试成功!

但由于我们的服务员是新来的,可能会发生这样的情况:

测试驱动程序模拟做饭: 期待一份热狗订单,然后给他这个假热狗作为回应

测试驱动程序(冒充客户)到服务员: 我想要一个热狗
服务员模拟做饭: 请给我一个汉堡
mock cook停止测试:我被告知要点热狗!

测试驱动程序注意到问题:TEST FAILED!服务员改了菜

测试驱动程序模拟做饭: 期待一份热狗订单,然后给他这个假热狗作为回应

测试驱动程序(冒充客户)到服务员: 我想要一个热狗
服务员模拟做饭: 请给我一个热狗
模拟做饭服务员: 点餐:1个热狗准备好(把假热狗递给服务员)
服务生测试驱动程序: 这是你的薯条(把另一份订单的薯条给测试司机)

测试驱动程序注意到意想不到的薯条:测试失败!服务员退错了菜

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

还要注意,这是一个非常简单的示例,模拟框架允许对组件的预期行为进行一些非常复杂的规范,以支持全面的测试。有很多关于模拟对象和模拟框架的材料可以获得更多信息。

For php and phpunit在phpunit文档中有很好的解释。在这里看到的 phpunit)文档 < / p >

简单来说,mock对象只是你的原始对象的虚拟对象,并返回它的返回值,这个返回值可以在测试类中使用

这是单元测试的主要视角之一。是的,您正在尝试测试您的单个代码单元,并且您的测试结果不应该与其他bean或对象行为相关。所以你应该通过使用mock对象和一些简化的相应响应来模拟它们。