最近看了两本关于测试的书,一本是 JUnit in Action,另一本是 Test-Driven Development: By Example。第一本简单讲了讲 JUnit 的一点点常用特性,感觉有点失望。第二本应该是 TDD 领域的经典之作,我在 O’Reilly 上看了英文版,受益匪浅。

测试很重要

虽然不是每一个工程师都会认真写测试,但是相信没有人会否认测试的重要性。Kent (Test-Driven Development: By Example) 的作者给了一张很形象的图,大意就是说,压力越大,人们就越倾向于不跑测试(也就意味着不写测试),而不跑测试又会让人压力更大。

压力与测试

从前粗浅的 TDD 理解

很久很久以前,我以为测试驱动开发就是多写测试。这个理解连肤浅都算不上,甚至都不正确。

在对 TDD 稍有了解之后,我觉得 TDD 就是指用测试(挂掉的测试)来驱动自己的开发,或者再通俗一点,先写测试,再实现功能。即使在读 TDD 这本书之前,我其实也能感受到这种开发方式的吸引力。先定一个小目标,然后心无旁骛的去实现它,是不是感觉非常有条理。

虽然直觉上觉得这种开发方式存在某种好处,但是我一直困惑的一点是,如果先写测试的话,岂不是会编译错误?接口和类都没有定义,我们到底应该在哪个阶段做这些设计决定?

TDD 到底是什么

实际上 TDD 包含简单的五个步骤

  1. 先写一个测试
  2. 运行所有的测试,这时刚写的测试一定会失败
  3. 做简单的改动,不管有多丑,尽快让失败的测试通过
  4. 运行所有的测试,保证所有的测试都通过
  5. 重构,删除重复的代码和数据

在更深入了解 TDD 之前,你可能以为 TDD 只是前四个步骤的不断循环,却忽略了非常重要的第五步。

这五个步骤非常简单,简单到没有尝试之前,你甚至都觉得这凭什么有这么多书籍和教程来鼓吹这种开发方式。但是在仔细思考(或者看过 TDD 这本书)之后,你会发现,实施这种做法会有一些迁移默化的作用,比如:

  1. 先写测试,导致你会以一个使用者的身份来思考,我需要什么样的接口。
  2. 为了尽快的通过测试,你会将很多的设计决定延后。
  3. 更多更频繁的重构。

在尝试实施了 TDD 之后,你会发现,当你优先测试的时候,你会更倾向于清晰的设计。

举个例子,你可能见过一些代码,使用了一个枚举型来处理复杂的情况,然后你会在很多地方找到 switch 这样的语句,来分别处理这些情况。这样的代码往往非常难以维护,一方面是因为可读性比较差,另一方面则是在之后如果要新加一种状态需要更改很多地方。

那么,如果你使用了 TDD 的开发方式会怎么样呢?如果你一开始就思考要怎么写测试,铺天盖地的 switch 显然不是一种容易测试的实现方式。你可能会使用多态来避免复杂的情况判断,同时大大的简化的测试。

总结

这样一篇短短的博客,很难详细的描述 TDD 的精髓。所以我强烈建议大家读一读 TDD 这本书,给它一个机会。