玩转模拟对象

687 查看

测试驱动开发(TDD)中,开发者经常使用模拟对象进行系统设计,模拟对象到底是什么呢?部分模拟对象和全部模拟对象又是什么呢?模拟对象真的让人又爱又恨吗?让我们以Objective-C测试框架OCMock来探个究竟。

模拟对象设计

模拟对象可以解决两种问题。第一种是(它们也是因此而提出的)用于设计测试驱动开发的测试类。想象一下,你已经完成了第一个测试,并知道了一些关于第一个类的API的信息。你的测试调用了新类的方法,你知道,应该从它们协作者之一种抓取一些信息。问题是,协作者尚不存在,而你又不想放弃这个已经设计出来的并开始测试的类。

此时,你可以创建一个模拟对象代表这个尚未“出生”的协作者。你可以设定你想要通过该“协作者”测试调用对象的期望值,而且,如果需要的话,还可以返回一个可以测试控制的值。你的测试可以验证你所期望调用的方法是否真的被调用了,如果没有,则测试失败。

在这种情况下,模拟对象就像一台VCR,只是没有上世纪八十年代的矮胖的造型和易受损的磁带。测试期间,模拟对象会记录你发送给它的每一条消息。然后,可以通过重放与消息列表做比较来看是不是你所需要的。就像用VCR,如果你想要看的是小精灵2(Gremlins 2),但是记录的却是上半年的新闻和欢乐酒店(Cheers),这就让人较为失望。

关键的部分是,你实际上并不需要建立真正的协作对象。事实上,你完全不需要关心它是怎么实施的。唯一需要关注的是它需要返回的消息,这样就可以验证他们是否被发送了。实际上,模拟对象可以让你觉得说,“我知道,在某些时候,我会考虑这一点,但我不希望因此而分心。” 对于测试驱动开发者,这就像一个待办事项清单一样清晰。

让我们来看一个例子。假设书呆子Ranch发现了市场上对博物馆库存管理App的需求。通常博物馆收藏了大量的文物,他们需要了解所有的库存,并能按主题,国家,年代等在画廊组织展览。关于库存的需求类似如下:

“作为策展人,我想知道所有需要展出的文物,这样我就可以给我的游客们讲故事了”。

我会写一个可以提供一个所有文物的清单的库存类用来测试。当然,磁盘上还有其他类也存储了所有的文物,但是我不关心他们是如何工作的,我只要创建一个库存接口的模拟对象。我的测试类如下:

为了让这个类编译通过,我需要创建BNRMuseumInventory类和它的initWithStore:和allArtefacts方法。

我还要定义BNRInventoryStore协议及其-fetchAllArtefacts方法,但我现在还不需要实现它们。为什么要我将它定义为一个协议,而不是另一个类?是为了提高灵活性:我知道我想发送给BNRInventoryStore的消息,但我并不需要关心它是如何处理这些消息的。使用协议能让我灵活的处理实现存储的方法:只要它能响应我所关心的消息,它可以是任何类型的类。

现在有足够的信息让编译器来编译和运行测试,但它还是不能通过。