我是单元测试的新手,我经常听到“模拟对象”这个词。通俗地说,有人可以解释什么是模拟对象,以及在编写单元测试时它们通常用于什么?
既然您说您是单元测试的新手,并且以“外行的术语”要求模拟对象,我将尝试外行的示例。
想象一下这个系统的单元测试:
cook <- waiter <- customer
通常很容易设想测试一个低级组件,例如cook:
cook
cook <- test driver
测试司机只需点不同的菜,并验证厨师为每个订单返回正确的菜。
更难测试一个中间组件,比如服务员,它利用了其他组件的行为。一个天真的测试人员可能会像测试厨师组件一样测试服务员组件:
cook <- waiter <- test driver
试驾员会点不同的菜,并确保服务员返回正确的菜。不幸的是,这意味着对服务员组件的测试可能依赖于厨师组件的正确行为。如果 cook 组件具有任何不利于测试的特性,例如不确定性行为(菜单包括厨师作为一道菜的惊喜)、大量依赖项(厨师不会在没有他的全部员工的情况下做饭)或大量资源(有些菜肴需要昂贵的食材或需要一个小时才能烹饪)。
由于这是一个服务员测试,理想情况下,我们只想测试服务员,而不是厨师。具体来说,我们要确保服务员正确地将顾客的订单传达给厨师,并将厨师的食物正确地交付给顾客。
单元测试意味着独立地测试单元,因此更好的方法是使用Fowler 所说的测试替身(假人、存根、假货、模拟)来隔离被测组件(服务员) 。
----------------------- | | v | test cook <- waiter <- test driver
在这里,测试厨师与测试驾驶员“勾结”。理想情况下,被测系统的设计使得测试厨师可以很容易地被替换(注入)以与服务员一起工作,而无需更改生产代码(例如,无需更改服务员代码)。
现在,测试厨师(测试替身)可以通过不同的方式实现:
有关 fakes vs stubs vs mocks vs dummies 的更多细节,请参阅Fowler 的文章,但现在,让我们专注于模拟厨师。
----------------------- | | v | mock cook <- waiter <- test driver
对服务员组件进行单元测试的很大一部分集中在服务员与厨师组件的交互方式上。基于模拟的方法侧重于完全指定正确的交互是什么,并检测何时出错。
模拟对象预先知道在测试期间应该发生什么(例如,将调用它的哪些方法调用等)并且模拟对象知道它应该如何反应(例如提供什么返回值)。模拟将表明真正发生的事情是否与应该发生的事情不同。可以为每个测试用例从头开始创建自定义模拟对象,以执行该测试用例的预期行为,但模拟框架努力允许在测试用例中直接清楚且轻松地指示此类行为规范。
围绕基于模拟的测试的对话可能如下所示:
试驾员 模拟 厨师 : 期待一份热狗订单并给他这个假热狗作为回应 测试司机 (冒充顾客)给 服务员 : 我想要一个热狗,请 服务员 模拟 厨师 : 1 个热狗,请 模拟厨师 给 服务员 : 订购:准备好 1 个热狗(给服务员假热狗) 服务员 来 测试司机 : 这是你的热狗(给测试司机假热狗) 测试驱动程序 :测试成功!
试驾员 模拟 厨师 : 期待一份热狗订单并给他这个假热狗作为回应
测试司机 (冒充顾客)给 服务员 : 我想要一个热狗,请 服务员 模拟 厨师 : 1 个热狗,请 模拟厨师 给 服务员 : 订购:准备好 1 个热狗(给服务员假热狗) 服务员 来 测试司机 : 这是你的热狗(给测试司机假热狗)
测试驱动程序 :测试成功!
但是由于我们的服务员是新来的,所以可能会发生这种情况:
试驾员 模拟 厨师 : 期待一份热狗订单并给他这个假热狗作为回应 测试司机 (冒充顾客)对 服务员 : 我想要一个热狗,请 服务员 模拟 厨师 : 1 个汉堡包,请 模拟厨师 停止测试: 我被告知期待热狗订单! 测试驱动程序 注意到问题:测试失败!- 服务员改变了订单
测试司机 (冒充顾客)对 服务员 : 我想要一个热狗,请 服务员 模拟 厨师 : 1 个汉堡包,请 模拟厨师 停止测试: 我被告知期待热狗订单!
测试驱动程序 注意到问题:测试失败!- 服务员改变了订单
或者
试驾员 模拟 厨师 : 期待一份热狗订单并给他这个假热狗作为回应 测试司机 (冒充顾客)给 服务员 : 我想要一个热狗,请 服务员 模拟 厨师 : 1 个热狗,请 模拟厨师 给 服务员 : 订购:准备好 1 个热狗(给服务员假热狗) 服务员 来 测试司机 : 这是你的炸薯条(给其他订单的炸薯条给测试司机) 试驾员 注意到意料之外的炸薯条:测试失败!服务员回错菜了
测试司机 (冒充顾客)给 服务员 : 我想要一个热狗,请 服务员 模拟 厨师 : 1 个热狗,请 模拟厨师 给 服务员 : 订购:准备好 1 个热狗(给服务员假热狗) 服务员 来 测试司机 : 这是你的炸薯条(给其他订单的炸薯条给测试司机)
试驾员 注意到意料之外的炸薯条:测试失败!服务员回错菜了
如果没有基于存根的对比示例,可能很难清楚地看到模拟对象和存根之间的区别,但是这个答案已经太长了:-)
另请注意,这是一个非常简单的示例,并且模拟框架允许对组件的预期行为进行一些非常复杂的规范,以支持全面的测试。有大量关于模拟对象和模拟框架的材料以获取更多信息。