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

发表于:2008-12-30 15:35

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:李晓    来源:ThoughtWorks

  接口存在的目的

  一个类实现
  对应一个接口
  为什么需要接口
  为了方便其它依赖这个类的类的测试
  为什么依赖这个类的类的测试需要使用接口
  因为可以或者只能使用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增加了你的测试代码的复杂度,想想我今天的话:)

22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号