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个热狗准备(给假热狗到服务员)
服务员,以试车手:这是你的炸薯条(从其他订单中给炸薯条测试司机)测试司机注意到意外炸薯条:测试失败!服务员给了错误的菜
可能很难清楚地看到模拟对象和存根之间的区别而没有基于存根的对比示例,但这个答案已经太久了:-)
另请注意,这是一个非常简单的示例,并且模拟框架允许从组件的一些非常复杂的预期行为规范来支持全面测试.有关更多信息的模拟对象和模拟框架有很多材料.
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)
请注意,warehouse
和mailer
模拟对象都使用预期结果进行编程.
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)
通过这种方式,我们可以轻松地测试依赖于其他类的类的驱动器开发.
当单元测试计算机程序的某些部分时,理想情况下,您只想测试该特定部分的行为.
例如,从一个虚构的程序片段中查看下面的伪代码,该程序使用另一个程序来调用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
模拟和存根对象是单元测试的关键部分.事实上,他们很长的路要走,以确保您正在测试单位,而不是群体为单位.
简而言之,您使用存根来破坏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()
失败则不会失败.