UML软件工程组织

测试驱动的开发,让你消除代码中的bug
作者: ZDNet China
 

项目后期检讨主要集中于发现了多少个bug以及如何处理它们。在大多数情况下,你会发现CMM(能力成熟度模型)、六西格玛(six sigma)等软件质量控制方法下过程评价基本上都是围绕着如何减少缺陷以及管理缺陷的。

然而,尽管编写没有bug的软件是如此的重要,许多时间还是花费在消除bug而不是花在如何防止它们的出现这一更重要的步骤上。在单元测试(Unit Test)中,绝大多数开发者在完成单元之后才开始进行测试,而不是开发代码的同时进行测试。没有人可以责怪程序员,因为项目计划常常给代码编写安排了最少的时间。之所以这样,是由于人们常常相信他们的计划非常完善,因此编写代码也就很简单。不幸的是,实际情况往往并不是这样的。

你的选择:测试驱动的开发

测试驱动的开发(Test-driven development,TDD)提供了一些新特性。TDD认为测试是一个在开发代码过程中的主要工作之一,是一个需要连续执行的过程。TDD一直需要与单元测试打交道,直到开发者在单元测试中确信他(她)的代码工作良好且可以保持这种良好的工作状态为止。TDD本质上是一种不依赖于任何特定工具的思想。它当然并不局限于Java,这一概念也可以应用于其它编程语言。不过,TDD在Java中用的最为广泛,因此在下文中我将仅结合Java介绍TDD。

在开始开发之前需要创建测试计划,TDD和普通方法的根本区别就是TDD并不会像后者那样往往成为一份被遗忘的Word或者Excel文档。使用TDD,如果你开发一个包含有复杂逻辑的Java类,那么测试代码(开发者负责编写测试代码)并不会也变得复杂起来。开发者常常抱怨他们不得不与Word和Excel打交道,但是在测试Java的情况下,甚至最挑剔的开发者编写测试也没有问题。

Kent Beck在他的《测试驱动的开发:实例》一书中描述了下面的TDD步骤:

  • 添加测试。
  • 运行所有的测试,测试未通过。
  • 修改代码。
  • 运行所有的测试,测试全部通过。
  • 重构(refactor)代码,消除冗余。
一个例子

为了演示TDD,让我们看看一段简单的代码(输入一个日期字符串,格式化后输出)。我们将遵循上面所提到的开发循环步骤。

我们在这个例子中使用JUnit测试框架。Junit帮助我们执行我们所编写的测试,并做一些基本的比较。如果你想使用Junit,那么在编写测试时需要遵循了若干规则。

我们假设我们所编写的方法可以获取以下格式的输入字符串:

  • 空值
  • MM-DD-YYYY
  • MM-D-YYYY
  • M-DD-YYYY
  • MM-DD-YY

上面的M表示月,D表示日期,Y表示年,MM表示用两位数字表示月份,以此类推。对所有这些可能的输入格式(空值除外),我们都将它转换为MM-DD-YYYY的格式;如果输入字符串为空,输出也为空。我们给只有一位数字的日期和月份在高位补上一个0,给两位数字的年份的高位补上20(例如,03年2月1号,按照“月-日-年”的格式表示为2-1-03,补齐后写为02-01-2003)。这样,我们现在就非常清楚的知道了代码应该实现怎样的功能。让我们开始执行上述步骤的第一步:编写测试。

假设

我们假设FormatDate是类的名字,formatDate是我们需要测试的静态方法的名字。

由于我们使用Junit框架,我们必须遵循它的某些惯例。如代码清单A所示,formatDate类将包含了我们所写的测试,我们需要把它扩展到类TestCase并导入来自junit.framework.*包的某些类。

代码

现在我们可以写测试了(甚至在我们实际编写被测试的代码之前,我们就可以编写测试)。我们所写的测试将驱动我们开发的代码。对每一种情况我们都需要写一个测试,如代码清单B所示。

这个简单的方法在执行时,将会调用formatDate方法,调用时,其参数为null。而assertNotNull方法将会检测formatDate方法的返回值是否为null。其它情况(如代码清单C所示)下的测试代码也与此类似。名字为assert*的方法由Junit框架所提供,它们使得期望值和实际值之间的比较变得简单了许多。

这样,我们就有了六个针对formatDate方法各种调用情况的测试。我们在FormatDateTester类中按照Juint的惯例编写了这些测试。在Junit下运行这些测试非常简单,如代码清单D所示。

formatDate方法在目前情况下非常简单,如下所示:
public static String formatDate(String strUnFormatted) {
 return null;
}

这样做仅仅是为了确保TestCase类可以通过编译。在基于Swing的测试台(Swing-based test runner)上运行这个测试之后,你将会看到如图A所示的画面。

图A

测试失败


粗红线表示某些测试未通过。在本例情况下,所有的测试均失败。这很好,因为我们已经完成了上面提到的步骤一和二。

现在,我们需要修改代码——或者说我们应该开始实际编写代码,并让它通过所有的测试。

首先,我们需要修改formatDate方法来让它通过测试。在你开发formatDate代码的过程中,定期的运行TestCase测试,你一般都会发现一两个测试不能通过。当你通过所有这些测试时,就停止编写代码。在我们早先所编写的测试的驱动下,现在的formatDate代码已经可以正确的完成我们所期望的功能,也就不需要再修改了。

开发周期的最后一步就是重构(refactor)代码,即消除任何冗余代码并合并所有与性能优化有关的变化。然后,再次运行你的测试。如果测试的结果仍为绿色(即所有测试均通过,如图B所示),那么代码编写的工作也就宣告结束了。.

图B

测试通过
setUp和teardown

在我们的例子中,我们测试了FormatDate类所提供的一个静态方法,在测试的过程中并没有代码的副本。然而,在某些情况下你不得不在所有的测试中重复性的例化类,那么可以把重复代码移到setUp方法来消除这些副本。框架会在执行任何测试之前自动调用这个方法。在测试结束之时调用teardown方法。你可以用这个方法来清除对资源的占用。

TDD的安全保证

及时的进行适当的测试是非常重要的,这样避免原开发者之外的其它人士修改源代码。你可以运行测试来确保原本可以正常工作的代码不会因为改动而崩溃。

基本上,一旦设计已经定稿并且某些开发已经完成,鼓起勇气修改设计可不是一件容易的事。当你重构代码时,有了适当的测试设备,你就可以安全的进行重构(refactoring)而不必担心对代码的修改会破坏任何东西。

理想情况下,你应该每天运行一组测试。看到那根代表测试通过的绿粗线,知道一天的任务业已圆满完成,你就会安心的睡个好觉。


附加工具
由于TDD的用法的增长,许多人会发现Junit有时不再适用了;当涉及到J2EE时,情况尤其如此。因此,许多Junit port应运而生,它们针对特定需求而扩展了Junit的核心概念。J2EEUnitStrutsTestCase以及Cactus就是其中的代表。

 

版权所有:UML软件工程组织