我是单元测试的新手,并且不断听到“模拟对象”一词泛滥的声音。用外行的话,有人可以解释什么是模拟对象,以及在编写单元测试时它们通常用于什么目的?
我是单元测试的新手,并且不断听到“模拟对象”一词泛滥的声音。用外行的话,有人可以解释什么是模拟对象,以及在编写单元测试时它们通常用于什么目的?
Answers:
既然您说您是单元测试的新手,并要求使用“外行的术语”来模拟对象,所以我将尝试一个外行的示例。
想象一下该系统的单元测试:
cook <- waiter <- customer
通常很容易想到测试像这样的低级组件cook
:
cook <- test driver
测试驾驶员只需订购不同的菜肴,并验证厨师是否为每个订单都返回了正确的菜肴。
测试利用其他组件行为的中间组件(如服务员)更加困难。天真的测试人员可能会以测试库克组件的相同方式来测试服务员组件:
cook <- waiter <- test driver
测试驾驶员将订购不同的菜肴,并确保服务员退回正确的菜肴。不幸的是,这意味着对服务员组件的测试可能取决于烹饪组件的正确行为。如果厨师部件具有任何测试不友好的特性,例如不确定性行为(菜单包括厨师给菜带来的惊喜),很多依赖项(如果没有整个员工都无法烹饪),则这种依赖关系会更糟。资源(某些菜肴需要昂贵的食材或需要一个小时才能烹饪)。
由于这是服务员测试,因此理想情况下,我们只想测试服务员,而不是厨师。具体来说,我们要确保服务员正确地将顾客的订单传达给厨师,并正确地将厨师的食物传递给顾客。
单元测试意味着独立地测试单元,因此更好的方法是使用Fowler所谓的测试双打(假人,存根,假货,假人)来隔离被测组件(服务员)。
-----------------------
| |
v |
test cook <- waiter <- test driver
在这里,测试厨师与测试驱动程序“纠缠不清”。理想地,设计被测系统,以便可以轻松地替换(注入)测试厨师以与服务员一起工作,而无需更改生产代码(例如,无需更改服务员代码)。
现在,可以通过不同方式实现测试厨师(测试两倍):
请参阅Fowler的文章,以了解有关假货,存根,模拟和假人的更多详细信息,但现在,让我们关注模拟厨师。
-----------------------
| |
v |
mock cook <- waiter <- test driver
对服务员组件进行单元测试的很大一部分集中在服务员如何与Cook组件进行交互上。基于模拟的方法着重于完全指定正确的交互作用,并检测何时出现问题。
模拟对象预先知道在测试期间会发生什么(例如,将调用其哪种方法调用等),模拟对象知道其应如何反应(例如,提供什么返回值)。模拟将指示实际发生的情况与预期发生的情况是否不同。可以从头开始为每个测试用例创建一个自定义的模拟对象,以执行该测试用例的预期行为,但是模拟框架努力使此类行为规范可以在测试用例中直接清楚地表明。
围绕基于模拟的测试的对话可能看起来像这样:
试车手,以模拟厨师:期待一个热狗秩序,给他回应这个虚拟热狗
测试驱动程序(冒充客户)服务员:我想一个热狗请
服务员来嘲笑煮:1个热狗请
模仿厨师到服务员:为了达到:1个热狗准备(给假热狗到服务员)
服务员,以试车手:这是您的热狗(给假热狗测试驾驶员)测试驱动程序:测试成功!
但是由于我们的服务员是新来的,因此可能会发生以下情况:
试车手,以模拟厨师:期待一个热狗秩序,给他回应这个虚拟热狗
测试驱动程序(冒充客户)服务员:我想一个热狗请
服务员来嘲笑煮:1个汉堡请
模仿厨师停止测试:有人告诉我,期待一个热狗订购!测试驱动程序注意到问题:测试失败!-服务员改变了顺序
要么
试车手,以模拟厨师:期待一个热狗秩序,给他回应这个虚拟热狗
测试驱动程序(冒充客户)服务员:我想一个热狗请
服务员来嘲笑煮:1个热狗请
模仿厨师到服务员:为了达到:1个热狗准备(给假热狗到服务员)
服务员,以试车手:这是您的炸薯条(以其他顺序提供炸薯条以测试驾驶员)测试驾驶员注意到了意想不到的炸薯条:测试失败!服务员给了错误的菜
如果没有与此相反的基于存根的示例来比较,可能很难清楚地看到模拟对象和存根之间的区别,但是这个答案已经太长了:-)
还要注意,这是一个非常简单的示例,并且模拟框架允许对组件的预期行为进行一些相当复杂的规范,以支持全面的测试。有关模拟对象和模拟框架的资料很多,以获取更多信息。
模拟对象是代替真实对象的对象。在面向对象的编程中,模拟对象是模拟对象,它们以受控方式模拟实际对象的行为。
计算机程序员通常会创建一个模拟对象来测试其他对象的行为,就像汽车设计师使用碰撞测试假人来模拟人在车辆撞击中的动态行为一样。
http://en.wikipedia.org/wiki/Mock_object
模拟对象使您可以设置测试方案,而不会占用大量笨拙的资源,例如数据库。您可以在单元测试中使用模拟对象来模拟数据库,而不必调用数据库进行测试。这使您免去了为建立类中的单个方法而建立和拆除真实数据库的负担。
“模拟”一词有时会与“存根”互换使用。这两个词之间的差异在这里描述。 本质上,模拟是存根对象,它还包含对被测对象/方法的正确行为的期望(即“断言”)。
例如:
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());
}
}
请注意,已为warehouse
和mailer
模拟对象编程了预期的结果。
模拟对象是模拟真实对象行为的模拟对象。通常,您在以下情况下编写模拟对象:
Mock对象是Test Double的一种。您正在使用模拟对象来测试和验证被测类与其他类的协议/交互。
通常,您将对“程序”或“记录”有某种期望:方法调用使您希望类对基础对象执行。
例如,假设我们正在测试一种服务方法来更新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());
这样,我们可以轻松测试依赖于其他类的类的驱动开发。
我强烈推荐Martin Fowler撰写的精彩文章,说明什么是模拟以及它们与存根的区别。
在对计算机程序的某些部分进行单元测试时,理想情况下,您只想测试该特定部分的行为。
例如,从一个虚构的程序中查看下面的伪代码,该程序使用另一个程序来调用print某些东西:
If theUserIsFred then
Call Printer(HelloFred)
Else
Call Printer(YouAreNotFred)
End
如果要进行测试,则主要是要测试用户是否为Fred的部分。您真的不想测试Printer
事物的一部分。那将是另一项考验。
这是Mock对象进入的地方。它们假装为其他类型的东西。在这种情况下,您将使用Mock Printer
,使其像真正的打印机一样工作,但不会进行打印之类的不便。
您可以使用不是Mocks的其他几种伪装对象。使Mocks Mocks成为主要对象的是,可以用行为和期望来配置它们。
预期会导致您的Mock在使用不当时引发错误。因此,在上面的示例中,您可能需要确保在“用户是Fred”测试案例中使用HelloFred调用了Printer。如果那没有发生,您的Mock会警告您。
Mocks中的行为意味着,例如,您的代码中的行为类似于:
If Call Printer(HelloFred) Returned SaidHello Then
Do Something
End
现在,您想测试调用Printer并返回SaidHello时的代码行为,因此您可以设置Mock在用HelloFred调用时返回SaidHello。
关于此的一个很好的资源是Martin Fowlers发表Mocks Are n't Stubs
模拟和存根对象是单元测试的关键部分。事实上,他们很长的路要走,以确保您正在测试单位,而不是群体为单位。
简而言之,您可以使用存根来打破SUT对其他对象的依赖(System Under Test),并通过模拟来做到这一点,并验证SUT是否对依赖项调用了某些方法/属性。这可以追溯到单元测试的基本原理,即测试应该易于阅读,快速并且不需要配置,这可能意味着使用所有实际类。
通常,您的测试中可以有多个存根,但是您只能有一个模拟。这是因为模拟的目的是验证行为,而您的测试只能测试一件事。
使用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()
失败不会使测试失败。
通过链接到“ Mocks Are n't Stubs ” 建议的另一个答案是,模拟是 “测试两倍”的一种形式,用于代替真实对象。使它们与其他形式的测试双打(例如存根对象)不同的原因在于,其他测试双打提供状态验证(和可选的模拟),而模拟提供行为验证(和可选的模拟)。
使用存根,您可以以任何顺序(甚至是反复地)在存根上调用多个方法,并确定存根是否已捕获您想要的值或状态,从而确定成功。相反,模拟对象期望以特定顺序甚至特定次数调用非常特定的函数。仅仅因为方法以不同的顺序或计数被调用,使用模拟对象的测试将被视为“失败”-即使测试结束时模拟对象的状态正确!
这样,通常认为模拟对象比桩对象更紧密地耦合到SUT代码。这可能是好事,也可能是坏事,具体取决于您要验证的内容。
对于php和phpunit,在phpunit documentaion中已作了很好的解释。看到这里 phpunit文档
用简单的话来说,模拟对象只是原始对象的伪对象,并返回其返回值,该返回值可在测试类中使用