测试驱动的编程是 XP 困扰程序员的一个方面。对于测试驱动的编程意味着什么以及如何去做,大多数人都做出了不正确的假设。这个月,XP 方面的讲师兼 Java 开发人员 Roy Miller 谈论了测试驱动的编程是什么,它为什么可以使程序员的生产力和质量发生巨大变化,以及编写测试的原理。请在与本文相随的 论坛中提出您就本文的想法,以飨笔者和其他读者。
最近 50 年来,测试一直被视为项目结束时要做的事。当然,可以在项目进行之中结合测试,测试通常并不是在 所有编码工作结束后才开始,而是一般在稍后阶段进行测试。然而,XP 的提倡者建议完全逆转这个模型。作为一名程序员,应该在编写代码 之前编写测试,然后只编写足以让测试通过的代码即可。这样做将有助于使您的系统尽可能的简单。
先编写测试
XP 涉及两种测试: 程序员测试和 客户测试。测试驱动的编程(也称为 测试为先编程)最常指第一种测试,至少我使用这个术语时是这样。测试驱动的编程是让 程序员测试(即单元测试 ― 重申一下,只是换用一个术语)决定您所编写的代码。这意味着您必须在编写代码之前进行测试。测试指出您 需要编写的代码,从而也 决定了您要编写的代码。您只需编写足够通过测试的代码即可 ― 不用多,也不用少。XP 规则很简单:如果不进行程序员测试,则您不知道要编写什么代码,所以您不会去编写任何代码。
如何先编写测试
整个理论很棒,但如何 先编写测试呢?首先,我推荐您阅读 Kent Beck 撰写的 Test-Driven Development: By Example(请参阅 参考资料)一书,里面列举了一个详尽的贯穿于整本书的示例。该书不仅讲述了如何编写测试和让这些测试来驱动您的代码的原理,而且还讲述了测试驱动的编程为什么是一种好的编程方法。这里我将举一个简单的例子,让您体会一下我正在讲什么。
假定我正在编写包含 Person 对象的系统。我希望在我问每个 Person 时,他/她能告诉我其年龄(作为整数)。即使我还没有编写一丁点代码,但也该编写测试了。“什么?”,您可能会说,“我甚至不知道在测试什么,怎么编写测试?”答案很简单,您 的确知道您在测试什么,只是不 知道您所了解的内容,因为您不习惯按这样的方式进行思考。这就是我的意思。
您确实还没有任何代码,但您脑海中应有 Person 对象的雏形。 Person 对象上应该有一个方法,该方法可以用整数形式返回年龄。因为我最常使用 Java 语言,所以我用 JUnit 来编写程序员测试。清单 1 显示了我为 Person 对象编写的 JUnit 测试:
清单 1. 用于 Person 对象的 JUnit 测试
package com.roywmiller.testexample; import junit.framework.TestCase; public class TC_Person extends TestCase { protected Person person; public TC_Person(String name) { super(name); } protected void setUp() throws Exception { person = new Person(); } public void testGetAge() { int actual = person.getAge(); assertEquals(0, actual); } protected void tearDown() throws Exception { } } |
首先,让我向那些不熟悉 JUnit 的人讲述一些浅显的原理。 TestCase 类是您将最常使用的类。您只是写了一个测试类(在该示例是 TC_Person ),它是 TestCase 的子类。(注:在 JUnit 3.8.1 中,可以有也可以没有接受 String 的构造函数,但由于我几乎所有的 Java 开发都在 Eclipse IDE(请参阅 参考资料)中完成,Eclipse IDE 免费向我提供了这个构造函数,所以我就把它保留在这里了。)一旦创建好测试类之后,测试方法中要有实际的动作。这些方法都恰如其分地用前缀 test 开头(它们必须是 public ,并且返回 void )。当运行测试时,JUnit:
内省测试类,并执行每个以“test”开头的方法
在执行每个测试方法之前执行 setUp() 方法
在执行每个测试方法之后执行 tearDown() 方法
在该示例中, setUp() 方法中没有太多要执行的语句。它只是实例化 Person (我用这个方法是让您觉得这个测试案例看上去很“完整”)。这意味着,如果这里有 20 个测试方法,则每个测试方法都以一个新的 Person 实例开始。 tearDown() 中不做任何事情,所以现在它是空的。值得强调的一点,您不需要 setUp() 或 tearDown() ;我通常直到编写第二个或第三个测试方法,并确定了这些方法都共享某些公共的设置或销毁活动时,才创建它们。
有了这些原理之后,要注意,我在测试方法中制订了一些设计决策。我假定,可以构造一个 person,并且“缺省” Person 会返回值为 0 的 age。还假定 Person 对象有 getAge() 方法。即使那些假定不会一直都成立,但目前它们还适用。可以说,这是一个简单的测试,让我说明测试驱动的编程。有了这些假定之后,实例化 Person (在 setUp() 中实例化 Person 只是为了展示如何使用 setUp() 方法),接着调用测试方法中正在测试的方法,然后调用其中一种“断言(assert)”方法。断言方法测试事情是否为 true。换句话说,这些方法针对某件事做出一个断言,该断言告诉 JUnit 验证该事是否为 true。表 1 列出了断言的类别:
表 1. 断言类别
断言方法 | 描述 |
assertEquals | 比较两件事物是否相等(基本类型或对象) |
assertTrue | 对布尔值求值,看它是否为 true |
assertFalse | 对布尔值求值,看它是否为 false |
assertNull | 检查对象是否为 null |
assertNotNull | 检查对象是否不为 null |
assertSame | 检查两个对象是否为同一实例 |
assertNotSame | 检查两个对象是否不为同一实例 |
在这里,我检查Person
实例的 age 是否为 0,新Person
对象的缺省值为 0。
当然,这个测试甚至不能编译。图 1 显示了当我试图在 Eclipse 上运行它时的 JUnit Fast View。