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

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

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

 作者:李晓    来源:ThoughtWorks

#
Mock
#
TDD

  前言

  我不是个反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模式进行测试,你会发现,真的是太脏了。

21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号