好消息是,测试驱动开发不仅仅适合于新代码。即使是程序员维护老代码时也可以利用它编写、运行以及通过测试。对于已经在生产中的遗留系统,测试确实更加 重要。只有通过测试,您才能确信您对于系统中的某一部分所做的改变不会中断其他地方的另外一部分。当然,您可能没有时间或者经费为一个规模庞大的代码基础达到 100% 的测试覆盖率,但是即使是并不完美的覆盖率也能减少失败的风险,加速开发并且产生更加健壮的代码。
本文使用 jEdit 做为例子,向您展示如何为从未测试过的遗留代码开发一个单元测试套件。jEdit 是一个流行的开放源码的文本编辑器,它完全没有任何测试套件!但是我将马上开始对其进行修改。在本文中,我着手开发一个测试套件,其目的是为了使将来 jEdit 的开发更加多产、高效并且有趣。
第一次测试
中国有句老话,千里之行始于足下。遗留代码的测试套件首先开始于一个单独的测试。重点是做什么和从何做起。不要掉入相信因为不能够测试每行代码所以就不能够测试任何东西的陷阱。只管打开您的 IDE 并且开始编写测试。使用 JUnit(或者 NUnit,或者 CppUnit,或者任何您喜欢的框架)和一个一般的 IDE,您通常就能够在 20 分钟以内编写出第一个测试。编写测试要比编写模型代码简单得多。测试很小并且具有独立的代码块。它们不需要很多配置、思考和理解。您不需要 “专业知识” 来艰难地编写出高质量的测试。
测试套件需要做的第一件事情是直接到达方法的中心。寻找您能够做的最大范围、最全面的测试。对于一个独立的应用程序,可能是 main() 方法。例如,这是我的第一个 jEdit 测试用例。它所做的就是运行应用程序的 main() 方法并且检验它是否在屏幕上输出了正确的窗口:
清单 1. 测试 jEdit 的 main() 方法
import java.awt.Frame;
import junit.framework.TestCase;
public class MainTest extends TestCase {
public void testMain() {
org.gjt.sp.jedit.jEdit.main(new String[0]);
// make sure there\'s a window on the screen
Frame[] allFrames = Frame.getFrames();
for (int i = 0; i < allFrames.length; i++) {
Frame f = allFrames[i];
if (f.isFocused()) {
assertTrue(f instanceof org.gjt.sp.jedit.View);
}
}
}
}
第一个测试的目的不是在边界条件上费力,也不是为了查看解决了什么问题。第一个测试是一个发烟试验,目的是为了对于什么可能是错误的给您一个清晰的概念。即使最基本的测试也不能揭示出构造系统、运行时环境、已安装的软件以及对每件事情进行本质上的破坏的其他主要问题中存在的问题。我的第一个测试用例确实能够准确地发现 jEdit 代码基础的这样一个问题:在我的类路径中没有包含所有可能的目录。
我并没有开始测试类路径配置,但是我寻找到的问题也是重要的,因为它可能导致代码基础很难调试。类似这种的全面测试涉及到应用程序的很多方面。很多不同的东西能够中断并且导致测试失败。就这种意义上说,并不是非常统一。在 test-first 编程中,这不是一个问题;但是当测试遗留代码时,您没有时间或者预算为每个单独的方法或者分支编写独立的测试。您必须在编写每个测试时尽量地覆盖尽可能多的方法和分支。使用一些测试来测试大部分代码比根本不进行测试要好。