在
面向对象程序设计中,模拟对象(英语:mock object,也译作模仿对象)是以可控的方式模拟真实对象行为的假的对象。
程序员通常创造模拟对象来测试其他对象的行为,很类似汽车设计者使用
碰撞测试假人来
模拟车辆碰撞中人的动态行为。
简介
模拟对象运用对象的模拟版本来代替被依赖的类(dependent classes),这些模拟对象被传递给要测试的类后,依赖关系就被对象的模拟版本代替了,而被测试对象则仍然会以为自己所处理的是真实的对象。
模拟对象框架之前,模拟对象都是手工编写的。
使用模拟对象的前提是:使用索依赖单元的接口必须定义清楚。
使用模拟对象原因
在
单元测试中,模拟对象可以
模拟复杂的、真实的(非模拟)对象的行为, 如果真实的对象无法放入单元测试中,使用模拟对象就很有帮助。
在下面的情形,可能需要使用模拟对象来代替真实对象:
例如,一个可能会在特定的时间响铃的闹钟程序可能需要外部世界的当前时间。要测试这一点,测试一直要等到闹铃时间才知道闹钟程序是否正确地响铃。如果使用一个模拟对象替代真实的对象,可以变成提供一个闹铃时间(不管是否实际时间),这样就可以隔离地测试闹钟程序。
技术细节
模拟对象具有和要模拟的真实对象的相同的
接口,可以让调用该接口的对象不知道在使用真实对象还是模拟对象。
现有的许多模拟对象框架允许程序员指定模拟对象上的哪些
方法,将按照什么顺序被调用,以及传入什么
参数,将返回什么值。这样,复杂对象(例如网络套接字)的行为将可以使用模拟对象来模拟,允许程序员来发现被测对象在可能各种存在的状态是否响应正确。
模拟对象,虚拟对象和桩
一些作者明确区分虚拟对象(fake)和模拟对象。虚拟对象比较简单,简单实现所代表的对象相同的接口,并返回预先安排好的应答。这样一来虚拟对象仅仅提供了一组方法桩。
在“单元测试的艺术”一书中,模拟对象被描述为帮助决定测试通过与否的虚拟对象,通过验证对象上是否发生了交互。其他的都被定义为桩。在该书中,“虚拟对象”(fake)是指所有非真实的对象。基于使用,或者是桩,或者是模拟对象。
从这个角度讲,模拟对象多做了一些工作:它们方法实现中包括
断言。这就是说,这个意义上的真正的模拟对象将会检查每个调用的上下文— 可能会检查器上方法的调用顺序,可能对方法调用的参数数据进行检验。
设定预期结果
考虑一个授权子系统被模拟的例子。模拟对象与真正的授权类相同,实现了isUserAllowed(task: Task):boolean方法。如果暴露一个真实对象中没有的属性isAllowed: boolean就会带来许多便利,测试代码可以很容易地设置预期的结果,用户通过授权,或没有通过,这样,两种情况下可以很容易地测试系统的行为。
同样,只有模拟对象才有的设置可以确保对子系统的后续调用将会导致异常抛出,或没有反应的
挂起,或返回
null等。这样,开发
客户端的行为时,可以对
后端子系统中的所有实际的故障的条件以及预期的响应进行测试。没有这样简单而灵活的模拟系统,对于每一种情形进行测试将是十分费力的。
记录日志字符串
一个模拟的数据库对象的保存方法save(person: Person)可能包含许多(如果有)实现代码。可能检查存在与否,可能
验证要保存的Person对象(参见上述的虚拟对象与模拟对象的讨论),但是除此以外可能没有其他的实现代码。
这就错过了机会,模拟方法可以记录一条日志到公共的的日志字符串。日志可以简单地写“Person saved”,也可以写person对象的详细信息,如名字或ID。这样,如果测试代码在对模拟数据库进行了一系列操作后检查日志字符串的最终内容,就能够验证数据库保存方法的执行次数是否与预期相符。 这种方法可以发现不可见的性能问题,例如一个开发人员对丢失数据感到紧张,编写了多次对save()的调用,而一次调用就已经足够了。
在测试驱动开发中的使用
使用
测试驱动开发(TDD)方法的程序员在编写软件时会使用模拟对象。模拟对象满足更复杂的真实对象的
接口需求,并代替真实对象的位置,有了模拟对象,程序员就可以对一个领域的功能性进行
单元测试,而不需要实际调用复杂的下层或协作的
类。使用模拟对象使得开发人员可以关注与被测系统(SUT)的行为的测试,而不需要担心被测系统的依赖关系。例如,测试在特定状态下一个基于多个对象的复杂算法,如果使用模拟对象代替真实对象可以很容易地表达出来。
除了复杂性问题和
关注点分离带来的好处,还有实际的速度问题。使用
测试驱动开发(TDD)开发一段实际的软件很容易就有数百个单元测试。如果这些单元测试中许多都涉及到与数据库,Web服务和其他
进程间通讯或
网络系统的通讯,单元测试的组合会很快会慢到无法执行例行测试。而这会导致坏的习惯以及程序员不愿意维护测试驱动开发的基本原则。
当模拟对象被替换为真实对象,端到端的功能仍需要进一步的测试。这将不再是单元测试,而是
集成测试。
局限性
模拟对象的使用可能会将单元测试与被测代码的实现耦合得很紧。例如,许多模拟对象框架允许开发人员指定模拟对象上方法被调用的次序和调用的次数,这样,测试通过后对代码进行
重构,即使方法依然遵守以前实现的契约,也可能会造成测试失败。这说明单元测试应当测试方法的外部行为,而非其内部实现。在单元测试测试用例中过度使用模拟对象可能导致随着系统的发展,不断进行的重构会造成维护测试本身的工作量出现显著的增长。在发展过程中,这种测试的不正确地维护可能会漏报错误,而在使用真实对象进行的测试中会捕捉到。相反,与设置好整个真实对象相比,简单地模拟一个方法可能需要更少的配置,因此减少了需要的维护工作。
模拟对象必须要准确地建模它们要模拟的对象的行为,然而,如果要模拟的对象来自另一个开发人员或项目,或者如果还没有开发出来,准确的建模是很难做到。如果没有正确建模行为,那么可能会单元测试记录通过,而真正运行时,在同样条件下可能会造成测试失败。