TDD(测试驱动开发,Test Driven Development)是重要的敏捷实践之一,它的基本原理是用测试来带动开发,先写测试代码,再写开发代码,最后重构。许多TDD推广和实践者认为,这种方式易于带来高质量的代码。而如今,TDD也慢慢有了Test Driven Design,也就是测试驱动设计的意味。也就是说,它更像是一种设计方式了。这些理论我很愿意相信,也很支持,但是从实际角度来说,我还是较难接受正统的TDD行为。不过,我也在实际开发过程中总结出……怎么说呢,应该说是更适合我自己的实践方式,在此希望能和大家交流一下。
我难以接受正统TDD方式的原因,在于我总是过于习惯在拿到一个需求的时候,在脑海里率先出现设计。而正统TDD的要求应该是先从测试代码开始,但是我脑海中已经出现了设计“草图”之后,写出来的测试也已经有相当明确的“导向性”了——那么,即使我先写测试,又有什么意义呢?而且,我在写测试的时候,总是在想“哎,这个测试真多余,反正最终代码不会仅仅是这样的”。对于我来说,我只能采取正统TDD方式的“形”,而实在接受不了它的“神”。
至今我还在疑惑,因为我觉得普通开发人员像我这样情况其实应该也有不少,那么对于像我这样的人,又该如何采用TDD的方式来开发项目呢?最终我放弃了使用TDD,不过单元测试是一定保留了下来的。
于是,我还是先写代码,再写测试,用测试来检查代码的实现和“期望”是否相符。接着,为了提高项目测试的可测试性,我会不断重构代码,分离职责,构造一些功能明确的辅助类等等。慢慢的慢慢的,似乎我觉得最后得到的成果还是相当有模有样的。忽然有一天,我觉得自己的做法也已经形成了一些“套路”,我一时兴起在推特上“宣称”我在使用一种叫做“测试导向开发”的方式,因为我时刻考虑代码该如何测试,为此而不断改变我的设计。
测试导向开发,即Test Targeting Development或TTD。当然最后一个D改为Design似乎也没有什么问题。
与传统TDD的开发方式不同,我的TTD方式还是先写代码,后写测试。只不过,我会时刻关注自己的代码是否容易测试,并不断重构产品代码和测试代码。基本上它的步骤是:
一般来说,这几个步骤的执行顺序都比较随意,唯一的目的便是在产品开发过程中,让产品代码得到更多的测试覆盖率。这会迫使我们编写更加容易测试的代码,而我慢慢发现这个要求很接近于著名的SOLID原则:
- 单一职责原则(Single Resposibility Principle):如果一个类的职责不单一,我写单元测试的时候就要准备一个复杂的初始数据,然后劳心劳力地推测出它的输出是什么。此时,我会把一部分职责抽象成外部类,然后再某种方式交由原来的类使用。在单元测试的时候,我可以为新生成的外部类构造Stub,也可以为这个外部类做额外的单元测试。
- 开/闭原则(Open/Close Principle):这个似乎和单元测试的关系不大,符合这个原则更多是为了更好的产品设计。当然,单元测试本身也需要产品提供一定的“开”点。
- 里氏替换原则(Liskov Substitution Principle):这个……和单元测试关系不大。
- 接口分离原则(Interface Segregation Principle):只有通过接口和具体实现类分离之后,才能在测试时为接口提供Mock或Stub。例如,把职责提取到外部类的时候,我会为外部类构建一个接口。而原来类要使用外部类的功能,便是通过接口来访问的。
- 依赖倒转原则(Dependency Inversion Principle):这个就不用说了,大大简化了单元测试的编写难度。值得注意的是,依赖注入不等同于“依赖注入容器”的使用。例如,我会为待测试的类添加一个用于注入辅助对象的构造函数,然后在单元测试时传入辅助对象的Stub。这其实也就是“依赖注入”。
在推特上“发布”我的TTD之后,有朋友告诉我说其实这也是TDD啊:Testability Driven Development,可测试性驱动开发。哎,真神奇。在下一篇文章中,我会使用一个简单的示例来展示“可测试性驱动开发”的实践方式,也希望能够引起更多更广泛的探讨。