谈“测试驱动的开发”

jopen 12年前

  现在的很多公司,包括 Google 和我现在的公司 Coverity,都喜欢一种“测试驱动的开发”(test-driven development)。它的原理是,在写程序的时候同时写上自动化的“单元测试”(unit test)。在代码修改之后,这些测试可以批量的被运行,这样就可以避免不应该出现的错误。

  这不是一个坏主意。我在 Kent 的编译器课程上也使用了很多测试。它们在编译器的开发中是不可缺少的。编译器是一种极其精密的程序,微小的改动都可能带来重大的错误。所以编译器的项目一般都含有大量的测试。

  但如果使用不当,测试却会大幅度的降低开发效率。当我给 Google 开发 Python 静态分析的时候,我几乎没有使用任何测试。虽然组里的成员催我写测试,但是我却知道那只会降低我的开发效率。因为这个程序在几个星期的过程中,被我推翻重来了好几次。要是我一开头就写上测试,这些测试就会碍手碍脚,阻碍我大幅度的修改代码。

  最后的结果是,我在 12 个星期之内,写出了 Google 一个小组的人需要几年才做得出来的东西。现在这个东西里面的技术,仍然处于世界领先地位。就连 Coverity 这种专门做静态分析软件的公司,代码质量也无法与之相比。按照他们的开发方式,要想在 12 个星期之内做出这个东西,是完全不可能的事情。

  测试的另一个副作用是,它让很多人对测试有一种盲目的依赖心理。改了程序之后,把测试跑一遍没出错,就以为自己的代码是正确的。可是测试其实并不能保证代码的正确,即使完全“覆盖”了也是一样。覆盖只是说你的代码被测试碰到过了,可是它在什么条件下碰到的却没法判断。如果实际的条件跟测试时的条件不同,那么实际运行中仍然会出问题。唯一能可靠的确保代码正确的方法是使用严密的逻辑推理,证明它的正确。

  很多人写程序只是凭现象来判断,而不能精密的分析程序的逻辑,所以他们修改程序经常“治标不治本”。如果程序出问题了,他们的办法是看看哪里错了,也不怎么理解,就改一下让它不再出错,最多再把所有测试跑一遍。或者再加上一些新的测试,用以保证这个地方下次不再出问题。

  这种做法的结果是,程序里出现大量的“特殊情况”和“补丁”。把一个“虫子”按下去,另一个虫子又冒出来。忙活来忙活去,最后仍然不能让程序满足“所有情况”。其实能够“覆盖所有情况”的程序,往往比能够“覆盖特殊情况”的程序简单很多。这是一个很奇怪的事情:能做的事越多,代码量却越少。也许这就叫做程序的“美”。

  美的程序不可能从修修补补中来。它必须完美的符合事物的本质,否则就会出现上面的情况,有许许多多无法修补的特例。程序员跟画家其实差不多,画家如果一天到头蹲在家里,肯定什么好东西也画不出来。程序员也一样,蹲在家里面对电脑,其实很难写出什么好的代码。你必须出去观察事物,寻找“灵感”,而不只是修改代码。在修改代码的时候,你必须用“心灵之眼”看见代码背后的事物。这也是为什么很多高明的程序员不怎么用调试器(debugger)的原因。他们只是用眼睛看着代码,然后闭上眼,脑海里浮现出其中信息的流动,所以他们经常一动手就能改到正确的地方。