不要把Mock当作你的设计利器

来自:ThoughtWorks   李晓

前言

我不是个反Mock者,Mock有它的优势,但使用它也同时带来风险,我认为使用Mock的基本原则是:不用。

不使用Mock,依赖一个设计简单、职责清晰的代码环境,因为只有简单的代码才能和Mock的主要优势相媲美,而使用这样的代码则可以避 免Mock所带来的麻烦以及风险,从而达到不用Mock并改进你的测试和代码设计的目的。

这里面最重要的,就是不要让你自己掉入Mock的陷阱,不要以为Mock就是最佳的解决方案,使用Mock和其所带来的设计复杂度以及Mock行为依赖风险是需要做权衡的。

TurtleMock 是最近我和一些同事一起在做的一个开源mock framework。使用TurtleMock所mock出来的Object,默认就是一个简单的NullObject实现,所有的方法都可以调用,返回 值按照java的类型默认值设计,只在你需要的 时候才去assert Mock Object做了什么。正是做TurtleMock促使我去思考为什么需要使用Mock,为什么我会更喜欢TurtleMock这种形式的Mock工具。 追根究底是因 为现有的Mock工具使用白盒测试的方式辅助测试,而这种Mock方式的大量使用影响到了我对实现的设计和重构。进一步的思考让我觉的目前在TDD过程中 对Mock的依赖性是不可取 的。

TDD (Test-Driven Development)不是Unit Test。相信无数人都提过这个话题,还是要在这里带一笔,因为我熟悉的是TDD,所有有关Unit Test的知识几乎都是来自于学习TDD的过程,所以我在下面的讨论都是基于TDD的,Unit Test就不讨论了,虽然我觉得也能成立。

文章中所有提及的“接口”一词,都是指Java中的Interface,所有基于代码的讨论,都是基于Java的,毕竟不同语言太多不同特性,难以一一陈述。

文章中所提及的“TestCase”通常指的是一个Test Class中的一个Test Method。

文章中所提及的“Domain”是一种泛指,它可以是包含你的所有业务逻辑的Domain Model,也可以是包含显示逻辑的Presentation;也可以把它简单地分类为除了UI、外部资源和第三方API之外的,我们自己设计实现并包含逻辑的代码。

你患有Mock依赖症吗?

你在TDD过程中,有多少测试是不用Mock的?
你的TestCase的set up是不是比TestCase本身复杂得多?
是不是满屏幕都是mock is expected to do something?
一旦进行重构,测试是不是因为Mock的强制约定而失败了一堆?
一旦重构代码,是不是到处需要修改你的测试所依赖的Mock?
一开始写测试,是不是就在想:我该Mock谁?
一碰到难以测试的情况,是不是脑袋里就转着“我有Mock我怕谁”?
出门见着朋友,是不是张口就是:你今天Mock了吗:p

在你明确了自己的阵营之后,下面我们会讨论下,这样的依赖为什么是不可取的。

Mock的优点

Mock Object的行为简单,简单到唯一,在set up好返回值后总是返回这个唯一值。

Mock Object的行为可以预期,调用到你不希望调用的方法会让测试失败,方法被调用了你还可以验证其参数。(TurtleMock和EasyMock可以生成一个简单的NullObject的Mock Object,可以忽略对方法的非预期调用)

可以Mock一些在真实环境下难以模拟或出现的错误或异常。

Mock 是一种白盒测试工具(TurtleMock在你不去assert Mock Object的行为时不是),传统的Mock Object的set up过程就是目标代码实现细节的设计过程(TurtleMock的set up 过程是你构造Mock Object行为的过程,它并不关心目标代码的设计实现)。(这个其实很难说是优点,它所带来的问题也是很明显的,见Mock的弊端2)

接口为使用者设计,所以接口还未实现。Mock可以让你以简单的方式验证使用者是如何使用接口的。

Mock的弊端

Mock Object的行为依赖风险。Mock Object的行为和真实对象的行为必须一致,这在你对真实对象进行重构的时候是很大的风险。即使当前Mock Object的行为和真实Object的行为完全一直,而且所有测试都覆盖到了,其结果也很可能是:代码一处修改,测试到处失败。实际开发过程中这种情况 是非常 常见的,而且已经有不少人依赖这一点来修改代码了。其方法是先修改代码让所有依赖这块代码的测试都失败,然后再一点一点修改测试代码让测试通过,这看起来 还非常不错。

Mock Object的set up过程过于繁重。为此,大多数Mock Object的set up过程都会在TestCase的环境set up过程中进行,由于Mock Object的set up直接导致如何对该Mock Object进行verify,这使得你的TestCase的set up过程实际上也在进行测试,测试的内容不但多而且难以分割成小的TestCase。从另一个角度说,过于复杂的Mock Object 的set up过程,也许说明你的代码承担的职责过多,分出更多小的职责清晰的类也许可以让你避免这样的情况出现。在实际应用中,太多的情况是,在一个 TestCase中,set up Mock Object的代码比其它的代码多得多。也许正是使用Mock勉强能够测试你当前的设计,让你止步不前;而一旦TestCase建立完整,过多的Mock 验证又让你的重构寸步难行;最终导致你一闭眼一蹬腿,忍了。

Mock Object 的set up过程通常难以阅读。由于Java在jdk1.5之前没有泛型支持,所以各种Mock工具的API都显得不尽人意。其中JMock提供的一套API是比 较受好评的,因为它使用起来简单明确,接近自然语言的使用习惯。但是即使有再好的API支持,当一个TestCase 需要多个Mock Object协助时,仍然会显得混乱而难以阅读,因为一个set up Mock Object的语句至少存在两个含义:
Mock Object的行为定义:
调用方法的返回值
调用方式时throw Exception
给调用方法时所传递的参数发送消息
TestCase期望assert的内容:
方法是否被调用以及调用的次数
调用方法时的参数是否合法
方法调用的顺序

Mock工具的存在还助长了一种坏味道,就是你的 接口很可能会迅速膨胀从而承担过多职责。传统的Mock工具在生成一个接口的Mock后,所有不希望调用的方法在被调用时是会导致测试失败的,所以你就会 忽略这个接口的膨胀,因为看起来一切还都在你的掌握之中,从而导致它承担过 多职责。这样的接口其典型的特征是一些Class使用这个接口的某一部分方法,另一些Class则使用其另一部分。这样的接口如果使用Self Shunt模式进行测试,你会发现,真的是太脏了。

接口存在的目的

一个类实现
对应一个接口
为什么需要接口
为了方便其它依赖这个类的类的测试
为什么依赖这个类的类的测试需要使用接口
因为可以或者只能使用Mock

太多接口存在的唯一目的就是为了测试,不是因为别的,就是因为容易Mock。这种设计复杂度的增加为测试提供了很大的帮助,没有它,是不是都没办法测试了?或者说,基本上已经是金科玉律了?在TDD大行其道的今天,可测试性高于一切的圣旨是不是太好用了?

接口有太多理由存在了,甚至有人提出面对接口编程,虽然那是对接口一词的片面理解,但是,为了方便Mock而从一个类抽取其所有public方法为一个 接口 的做法,真的应该吗?我实在厌倦了这种不得已的选择,被Mock套上了枷锁,蒙蔽了双眼,直至今天才重新审视,原来自己需要的是鼓足勇气去重构。扔掉 Mock再披荆斩棘,似乎有点破釜沉舟的味道,但也不是什么上青天的难事。

你的代码为测试做好准备了吗?

似乎非常显然,TDD的产物,难道还没有为测试做好准备?那么:你的Object容易创建吗?如果你的Object难以创建,你需要Mock。

你创建的Object行为容易预测吗?或者说,它的行为逻辑复杂吗?如果你创建的Object行为分支过多逻辑复杂,你需要Mock。

你创建的Object职责是不是只有一样?如果你创建的Object职责很多,而你当前要进行测试的目标class只会牵涉其部分职责,你需要Mock。

越是简单的东西,越是容易被测试,也越容易被用于测试,没有复杂的分支,就不需要你去考虑这样的情况怎么样,那样的输入数据又会怎样。

Mock的最大优势是什么?就是它的行为固定,它确保当你访问该Mock的某个方法时总是能够获得一个没有任何逻辑的直接就返回的结果。所以一个容易创 建行为固定的简单 Object是很容易在测试中使用的。相信马上有人会认为这样的简单是难以达到的,因为总是有难以预料的复杂存在,以至于你直接就告诉我,那是不可能的。 在这里我难以一一列举每种情况以及每种情况的对策,一个简单的原则是,使用多态解决多分支,使用更多的小类小单元替代大的复杂的类。大小的衡量标准?你的 测试。这需要你深入挖掘你的Domain,把所有单元分到足够小,有时候你的一个复杂类的产生,纯粹是由于概念上难以细分,实现一个由模糊的概念衍生的 Domain Object,往往会导致该对象的复杂度增加。当然,现实是很多Domain Object没有真正去做之前,只能有个模糊的概念,手里的需求往往是功能级的描述,设计实现正是你要做的事情,如果你不能一眼看穿其本质,那么实现的过 程中就总是会有意外的惊喜出现,没有关系,你有至少两个选择:

做Spike,无论如何,先证明你的想法是能走通的,通过 Spike把细节挖掘清楚,然后勇敢地把所有Spike代码删掉,一点一点地通过TDD重新实现。对于TDD的初学者我认为这个是非常必要的,因为我觉得 做TDD,很大一部分比拼的就是对细节的挖掘能力。犹如庖丁解牛,对细节了如指掌,自然能游刃有余。

直接TDD去实现,仔细分析并 建立 完整的to do list,每一步都要有勇气去做放弃或者规模较大的重构,让实现慢慢清晰起来,大胆地分离职责,而不是任由目标class的职责膨胀,仅通过不断加 test case来完成所有的需求。这需要时刻把握目标class的唯一职责。很多人在学TDD的时候总是会问,到底TDD的一个Step多大合适?我觉得对于任 何人来说,越小越合适,大的Step是很诱人且看起来是很容易做到的(太难做好),小的Step则看起来让人有些无从下手但找到下手的地方后就容易了。而 能让你选择大的Step的唯一理由是,你的脑子转得够快,也就是说,在你的大脑中已经 完成了所有的小Step,对你来说,一个稍大点的Step已经是显然的了。通常这种情况我认为在重构的过程中非常多,实际在TDD中,反而有些不需要,因 为如果你能做大的Step,那么小的Step对你来说,只是不去动脑筋罢了,省力很多:) 当然,仍然很多人认为去动脑子比动手省力,我每次有这样的念头时都会被难以通过的test case郁闷到:(

回归Mock

Mock我们仍然是需要的,在我们遇到如下问题时,Mock是我们的第一选择:

外部资源,比如文件系统(Java的文件系统接口少,难以Mock,不过现在已经有不少开源项目专门做了内存的文件系统用于测试,比如cotta),这是因为对此类外部资源依赖性非常强,而其行为的不可预测性很可能导致测试的随机失败。

UI,这个实际上和外部资源也搭得上边,因为UI很多时候需要用户行为触发事件。MVC和MVP模式都很好地解决了这个问题。

第三方API

当接口属于使用者,通过mock该接口来确定测试使用者与接口的交互,明确定义该接口的职责。

在处理这些问题的过程中,特别是面对外部资源和第三方API时,Mock的风险是比较大的,多做Spike,为对应行为建立一组Acceptance测试是一个好的选择。

显然在你建立的Domain内部,你不应该去想着用Mock,不应该去想该不该 用Mock,念头也不要动。你可以通过使用Observer去隔离对UI的依赖,通过Proxy去隔离对数据持久层的依赖,通过Adapter、 Proxy或者Stairway to heaven模式去隔离对第三方API的依赖,简单地说,Domain用到什么难以测试的外部包,使用接口隔离,把接口留在Domain里让依赖倒置,让 其它API去依赖Domain,提高你的Domain的独立性和可测试性,让你的代码真正为测试做足准备,从而在Domain里脱离Mock的苦海。

相信很少有人真正有心去读Kent Beck的《TDD》一书中Part 1 — Chapter 17 Money Retrospective中的Code Metrics,这个表格中的第四行:Cyclomatic complexity ([3])?1.04?1

理解这里面的1.04和1所代表的意义,你也就理解我现在的感慨。也许有人认为Money这个例子太理想化了,但是又有多少人能够在完成Money这个 例子 时能够达到这样的标准;曾几何时,TDD也是那么遥不可及。无论如何,如果使用Mock增加了你的测试代码的复杂度,想想我今天的话:)

References
Kent Beck. Test-Driven Development By Example. Reading, Mass.: Addison-Wesley, 2002.
Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices. Reading, Mass.: Prentice Hall, 2002.
Introduction to Test Driven Development(TDD)
更多的TDD相关资料可以从这里找到:http://www.testdriven.com/
Tim Mackinnon, Steve Freeman and Philip Craig pioneered the concept of Mock Objects, and coined the term. They presented it at the XP2000 conference in their paper Endo Testing: Unit Testing with Mock Objects
JMock
EasyMock
TurtleMock
这里可以找到有关极限编程(XP)的详细介绍:http://www.extremeprogramming.org/


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值