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

2022-03-26  本文已影响0人  不再更新_

原文 ThoughtWorks 的李晓在 06 年发表在 CSDN 上的,现在原始链接不知道为什么删除了,好在网上有其他人转载过,复制一份过来重新排版一下,作为备份。

前言

我不是个反 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 依赖症吗?

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

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 在 JDK 1.5 之前没有泛型支持,所以各种 Mock 工具的 API 都显得不尽人意。其中 JMock 提供的一套 API 是比较受好评的,因为它使用起来简单明确,接近自然语言的使用习惯。但是即使有再好的 API 支持,当一个 TestCase 需要多个 Mock Object 协助时,仍然会显得混乱而难以阅读,因为一个 set up Mock Object 的语句至少存在两个含义:

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

上一篇下一篇

猜你喜欢

热点阅读