【翻译】编写可测试代码的艺术(一)

原文:http://misko.hevery.com/code-reviewers-guide/
按个人理解翻译,有哪里理解不对通过邮箱给我留意见: ccnyou@qq.com
还在持续翻译中,先发整体框架上来…

缺陷一:构造函数做太多工作

构造函数中去创建/初始化合作者,与其他服务通讯,初始化自己的逻辑状态,这些做法会移除插入测试所需要的接缝,强制子类/mock继承一些不想要的行为。构造函数中做太多的事情会阻碍测试模块初始化和更换合作者。

警告标志:

为什么这是缺陷

当你的构造函数不得不实例化和初始化它的合作者,这是一种不灵活和过早耦合的设计,这种构造函数剥夺了测试工具注入测试合作者的能力。

这违反了单一职责原则。
当合作者的构造与初始化混合时,它建议只有一种方式配置类,这会关闭可能以其他方式可用的重用机会。对象图创建是一个完全成熟的责任 - 首先这就不同于类的实例化。在构造函数中执行此类工作违反了单一责任原则。

很难直接测试
测试这种构造函数很困难。为了初始化一个对象,构造函数必须执行。如果构造函数做了很多工作,当你测试时候创建对象也将被迫做这些工作。如果构造函数访问了外部资源(例如文件,网络服务,数据库),合作者的一点点改变都会反映在构造函数中,但是很可能会被遗漏,由于没有测试代码覆盖,因为构造太难测试。我们最终陷入了一个恶性循环。

子类化和重写函数测试依旧是有缺陷的
有时候构造函数自己只做一点点工作,但是委托了一个需要测试子类覆盖的方法。这可以解决很难构造这个问题,但是“子类化以测试”这个黑魔法应该作为最后采取的手段。另外,通过子类化,你会没法测试那些你覆盖了的方法。那些方法做了很多工作(记住 - 这是为什么它从一开始就被创建),所以这或许应该被测试。

这会给你强加合作者
有时候你在测试一个对象,你不想实际创建它所有的合作者。例如,你不想要一个会跟MySQL服务器交互的MySqlRepository对象。然而,如果它们在你的被测试系统里面是直接用new MySqlRepositoryServiceThatTalksToOtherServers()创建,你将被迫使用这种重量级的对象。

这会移除测试所需要的”裂缝”
裂缝是指那些你可以切分你的代码库以移除依赖,能够实例化出小的,集中的对象。当你在构造函数中用 new XYZ(),你将没法得到一个不同的(子类)对象(看Michael Fathers的书《修改代码的艺术》了解更多关于裂缝的细节)。

即使你有多个构造函数这仍然是个缺陷(例如专门给测试写的”Test Only”构造函数)
创建一个单独的 “Test Only” 构造函数并没有解决这个问题。那些做了很多工作的构造函数依旧会被别的代码使用。即使你可以在隔离的环境测试这个对象(用测试特化的构造函数),你将遇到其他用了很难测试的构造函数的类。当你遇到其他这种类的时候,你会感觉束手无策。

底线
这一切都要看使用隔离的或者测试的合作者创建类是否容容易。
* 如果这很难,你在构造函数做了太多的工作了!
* 如果这很简单,恭喜你自己吧。

当你在写对象代码的时候,需要一直思考这个对象有多难测试。是否可以通过你正在写的构造函数实例化它?(记住,这个类不会仅仅在你的测试代码被初始化。)

很多设计都充满了 “实例化其他对象或者从全局检索其他对象的对象,这些编程实践,如果缺少检查,会导致高耦合的设计,难以测试”。[J.B. Rainsberger, JUnit Recipes, Recipe 2.11]

识别这个缺陷

评估这些症状: * 是否用new 关键字创建了任何你想要在测试时候替换的对象?(通常不是简单的值对象) * 是否包含任何的静态方法调用?(记住:静态方法调用时不可mock的,也不可注入的,所以如果你看到 Server.init() 或者任何类似的,警钟就已经敲响了!) * 是否包含任何的条件或者循环逻辑?(每次实例化对象的时候,你都不得不成功地梳理逻辑,这回导致过多的初始化代码,不仅当你需要直接测试这个类,还包括测试跟它相关的类)

考虑一个基本的问题,当你写或者review代码时: 我要怎么测试这些代码?

“如果答案不明显,或者测试代码看上去会很丑陋或者很难编写,这就是一个警告的信号。你的设计可能需要被修改;
改变一些东西直到代码容易被测试,通过你的努力你的设计最终会变得更好”
[Hunt, Thomas. Pragmatic Unit Testing in Java with JUnit, p 103 (somewhat dated, but a decent and quick read)]