Mocks Aren't Stubs
原文:https://martinfowler.com/articles/mocksArentStubs.html
这篇是Martin Fowler 大神对于单元测试的概念的分析,收获颇多。
该篇文章解释mock对象如何工作,如何鼓励基于行为的测试,社区如何围绕他们发展出不同风格的测试。
第一次听说术语“mock object” 是一些年前在极限编程社区,但人们没有很好的描述它,而且经常与stub混淆,差异主要在两方面,一方面是测试结果的教研不同:状态校验和行为校验。另一方面,与测试和设计共同发挥作用的方式截然不同,这里我描述为classical和mockist TDD风格。
常规测试
订单包含商品和数量,仓库保存不同的商品和数量,然后用仓库填充订单,2种不同的响应,商品足够填充订单,订单填充,仓库商品减少,商品不足,订单失败,仓库不变。XUnit 测试遵循典型4个阶段顺序,setup,exercise,verify,teardown。例子中订单是测试对象,我使用System Under Test,仓库是一个配合对象。这个风格是状态校验(state verification)
Mock对象的测试
setup阶段有些不同,分成2部分,data和expectations
关键不同点是我们通过订单的反馈来校验它是否做了对的事情,状态校验通过仓库的状态判断。Mock使用行为校验。
使用EasyMock
EasyMock使用 记录/回放 (record/replay)隐喻去设置期望。每一个对象创建一个control和mock对象。可以添加额外的特性,可以调用实际的方法,不用传方法名。
Mocks与Stubs的不同
当你专注于一个软件元素时,也就是我们通常的单元测试,做一个单一的单元测试的问题时,我们经常需要其他的单元,因此我们的例子中需要仓库。
在之前我们展示的2种测试风格中,第一个例子使用真实的仓库对象,第二个使用模拟的仓库对象,使用mocks是一种不适用真实仓库的方法,但还有其他形式不使用真实对象的方法。
在讨论这些的时候词汇容易混乱,stub,mock,fake,dummy。这里我们使用Gerard Meszaros的书。Mezaros使用术语Test Double代表任何类型的假装对象,来替换真实对象。
Dummy对象是传递,但不会实际使用。通常他们知识填充参数列表。
Fake对象实际工作与实现,但通常使用一些捷径,不满足生产(比如内存数据库)
Stubs提供一个调用的罐头类回答,通常不回答任何外界的要求。
Spies当他们被调用的记录一些信息,一种形式是email服务记录有多少消息发送
Mocks编程前给予期望,特定调用的响应。
差异的选择
文章中已经讨论了不同:/状态或行为校验 /classic or mockist TDD。选择行为状态 vs 行为呢?
第一个是考虑上下文。如果时间简单的配合,没有什么选择。mockist就是mock对象,行为校验。classicist确实需要做选择,但不是特别大,通常是一个个的解决。
cache是一个比较难classical的方式去解决,mocj是一个好方式。
Driving TDD
Mock对象是从XP社区出来的,而且重点发展TDD是XP的特性原则之一,那里系统设计通过写测试迭代发展。
因此不用惊讶mockists,尤其是mockist testing 在设计上的效果的谈论,尤其他们贡献了一个叫need-driven development 的方式。通过这种方式,你通过写一个测试开始你的用户故事,给你的SUT制定一些接口,通过合作者的一些期望,你开放SUT与关联对象的关系-有效设计SUT的关联接口。
一旦你有了一个测试在运行,在mocks对象上的期望提供下一步的规范和测试的起点。你一个添加测试上的期望在合作者上,然后重复过程在SUT上,这种方式在分层系统中工作得很好。你第一步写UImock,然后写耕更低层次的测试用例,逐渐写每一层,这是一种结构化和可控的。很多人相信对于新的面相对象和TDD新人是有帮助的。
Classic TDD没有提供这种指导,你可以做相似的步骤,使用stub方法替代mocks。当你需要合作者的时候,你写死代码来响应SUT的工作。
但是classic TDD可以做另外的事情,一种通常的风格是middle-out。在这种风格,你做一个特性,决定你需要在domain中。
我应该强调mockists和classicists做了同一件事情,有这么一个学校教大家,一层层去构建系统,不是开始一层指导另外一层完成。classicist和mockist都趋向于敏捷背景和更容易获得的迭代循环,结果他们逐个特性地工作而不是逐层工作。
夹具设置
Classic TDD 你需要创建的不仅仅是SUT,同时也要创建所有的合作者来响应SUT测试,测试通常会调用一大堆合作对象,通常这些对象每次测试被创建和销毁。
Mockistest只要创建SUT和模拟相关的邻居,者可以避免创建复杂的夹具。
实际中,classic测试者趋向于重用复杂的夹具。最简单的方式是将夹具放在xUnit setup方法中,更复杂的夹具,几个测试类都需要用的,需要生成类,我通常称他们是Object Mother
结果我听到双方都控诉对方做了太多的工作,mockists说创建夹具做了太多的努力,但classicist说这个可以重用,但你不得不每次都创建mocks。
测试隔离
如果你通过mockist测试引入一个bug,它通常只引起SUT包含bug的失败。通过classic的方式,很多相关的可能都失败,如果一个重用的对象,那会导致很多的失败。
Mockist测试者因为这是一个主要的问题,它导致很多debugging,为了发现错误的根源与修复它。但classicist看起来不是什么问题,因为一般是最后编辑的代码有问题,你很快会发现。
测试粒度是一个重要的因素。因为classic测试执行多个真实对象,你经常会发现单个测试有一簇对象,很难发现真实bug的源码,测试粒度太粗会发生。
本质上xunit测试不仅仅是单元测试,也包括最小化的集成测试。
实现与测试耦合
当你写mockist测试,你需要调用SUT,确保它提供的正确的,classic测试只关心最终状态,不关心状态如何传递,Mockist测试更多与实现耦合,改变合作方法会导致mockist测试损坏。
这些耦合导致一些关注,最重要的是对TDD的影响。mockist测试,书写测试要确保实现的行为,相反mockist测试者把这个看成优点。Classicists,认为只关心外部的接口,留下所有实现关联直到测试写完。
设计方式
这些测试方式最让我着迷的方面是他们如何影响设计决定。
我已经提到一些不同层面的踪迹。Mockist测试者支持从外到内的实现,classic测试更倾向于domain model的方法。
该选择classicist或mockist ,我发现这是一个难回答的问题。