软件开发习惯中一个细微更改都可能会对软件质量产生巨大改进。将单元测试合并到开发过程中,然后从长远角度来看它可以节省多少时间和精力。本文通过使用代码样本说明了单元测试的种种好处,特别是使用 Ant 和 JUnit 带来的各种方便。
测试是大型开发过程中的基本原则之一。在任何职业中,验证都是一个重要部分。医生要通过验血来确诊。波音公司在研制 777 的过程中对飞机的每个组件都进行了精心测试。为什么软件开发就应该例外呢?
以前,由于在应用程序中将 GUI 和商业逻辑紧密联系在一起,这就限制了创建自动测试的能力。当我们学会通过抽象层将商业逻辑从界面中分离出来时,各个单独代码模块的自动测试就替代了通过 GUI 进行的手工测试。
现在,集成开发环境 (IDE) 能在您输入代码的同时显示错误,对于在类中快速查找方法具有智能探测功能,可以利用语法结构生成彩色代码,而且具有许多其它功能。因此,在编译更改过的代码之前,您已经全盘考虑了将构建的类,但您是否考虑过这样的修改会破坏某些功能呢?
利用 Ant 和 JUnit 进行增量开发 Malcolm Davis
软件开发习惯中一个细微更改都可能会对软件质量产生巨大改进。将单元测试合并到开发过程中,然后从长远角度来看它可以节省多少时间和精力。本文通过使用代码样本说明了单元测试的种种好处,特别是使用 Ant 和 JUnit 带来的各种方便。 测试是大型开发过程中的基本原则之一。在任何职业中,验证都是一个重要部分。医生要通过验血来确诊。波音公司在研制 777 的过程中对飞机的每个组件都进行了精心测试。为什么软件开发就应该例外呢? 以前,由于在应用程序中将 GUI 和商业逻辑紧密联系在一起,这就限制了创建自动测试的能力。当我们学会通过抽象层将商业逻辑从界面中分离出来时,各个单独代码模块的自动测试就替代了通过 GUI 进行的手工测试。 现在,集成开发环境 (IDE) 能在您输入代码的同时显示错误,对于在类中快速查找方法具有智能探测功能,可以利用语法结构生成彩色代码,而且具有许多其它功能。因此,在编译更改过的代码之前,您已经全盘考虑了将构建的类,但您是否考虑过这样的修改会破坏某些功能呢? 每个开发者都碰到过更改“臭虫”。代码修改过程可能会引入“臭虫”,而如果通过用户界面手工测试代码的话,在编译完成之前是不会发现它的。然后,您就要花费几天的时间追踪由更改所引起的错误。最近在我做的一个项目中,当我把后端数据库由 Informix 更改到 Oracle 时就遇到了这种情况。大部分更改都十分顺利,但由于数据库层或使用数据库层的系统缺少单元测试,从而导致将大量时间花费在尝试解决更改“臭虫”上。我花了两天的时间查到别人代码中的一个数据库语法更改。(当然,那个人仍是我的朋友。) 尽管测试有许多好处,但一般的程序员对测试都不太感兴趣,开始时我也没有。您听到过多少次“它编译了,所以它一定能用”这种言论?但“我思,故我在”这种原则并不适用于高质量软件。要鼓励程序员测试他们的代码,过程必须简单无痛。 本文从某人学习用 Java 语言编程时所写的一个简单的类开始。然后,我会告诉您我是如何为这个类编写单元测试,以及在编写完它以后又是如何将单元测试添加到构建过程中的。最后,我们将看到将“臭虫”引入代码时发生的情况。 从一个典型类开始 清单 1. 我的第一个 Java 应用程序 "Hello world"
类开发 更简单的过程
接下来我们将这个单独的单元测试对象放入构建过程中。这样,我们就可以提供自动确认过程的方法。
使用 JUnit 自动化单元测试
测试布局 图 1. TestSuite 布局 测试类 HelloWorldTest.java 清单 2. HelloWorldTest.java
清单 3. Hello world 测试案例。
如果我保留了
新的 使用 Ant 将测试集成到构建中
图 2 简要介绍了一个配置文件。配置文件由目标树构成。每个目标都包含了要执行的任务,其中任务就是可以执行的代码。在本例中,mkdir 是目标 compile 的任务。mkdir 是建立在 Ant 中的一个任务,用于创建目录。 Ant 带有一套健全的内置任务。您也可以通过扩展 Ant 任务类来添加自己的功能。 每个目标都有唯一的名称和可选的相关性。目标相关性需要在执行目标任务列表之前执行。例如图 2 所示,在执行 compile 目标中的任务之前需要先运行 JUNIT 目标。这种类型的配置可以让您在一个配置中有多个树。 |
利用 Ant 和 JUnit 进行增量开发 Malcolm Davis
软件开发习惯中一个细微更改都可能会对软件质量产生巨大改进。将单元测试合并到开发过程中,然后从长远角度来看它可以节省多少时间和精力。本文通过使用代码样本说明了单元测试的种种好处,特别是使用 Ant 和 JUnit 带来的各种方便。 测试是大型开发过程中的基本原则之一。在任何职业中,验证都是一个重要部分。医生要通过验血来确诊。波音公司在研制 777 的过程中对飞机的每个组件都进行了精心测试。为什么软件开发就应该例外呢? 以前,由于在应用程序中将 GUI 和商业逻辑紧密联系在一起,这就限制了创建自动测试的能力。当我们学会通过抽象层将商业逻辑从界面中分离出来时,各个单独代码模块的自动测试就替代了通过 GUI 进行的手工测试。 现在,集成开发环境 (IDE) 能在您输入代码的同时显示错误,对于在类中快速查找方法具有智能探测功能,可以利用语法结构生成彩色代码,而且具有许多其它功能。因此,在编译更改过的代码之前,您已经全盘考虑了将构建的类,但您是否考虑过这样的修改会破坏某些功能呢? 每个开发者都碰到过更改“臭虫”。代码修改过程可能会引入“臭虫”,而如果通过用户界面手工测试代码的话,在编译完成之前是不会发现它的。然后,您就要花费几天的时间追踪由更改所引起的错误。最近在我做的一个项目中,当我把后端数据库由 Informix 更改到 Oracle 时就遇到了这种情况。大部分更改都十分顺利,但由于数据库层或使用数据库层的系统缺少单元测试,从而导致将大量时间花费在尝试解决更改“臭虫”上。我花了两天的时间查到别人代码中的一个数据库语法更改。(当然,那个人仍是我的朋友。) 尽管测试有许多好处,但一般的程序员对测试都不太感兴趣,开始时我也没有。您听到过多少次“它编译了,所以它一定能用”这种言论?但“我思,故我在”这种原则并不适用于高质量软件。要鼓励程序员测试他们的代码,过程必须简单无痛。 本文从某人学习用 Java 语言编程时所写的一个简单的类开始。然后,我会告诉您我是如何为这个类编写单元测试,以及在编写完它以后又是如何将单元测试添加到构建过程中的。最后,我们将看到将“臭虫”引入代码时发生的情况。 从一个典型类开始 清单 1. 我的第一个 Java 应用程序 "Hello world"
类开发 更简单的过程
接下来我们将这个单独的单元测试对象放入构建过程中。这样,我们就可以提供自动确认过程的方法。
使用 JUnit 自动化单元测试
测试布局 图 1. TestSuite 布局 测试类 HelloWorldTest.java 清单 2. HelloWorldTest.java
清单 3. Hello world 测试案例。
如果我保留了
新的 使用 Ant 将测试集成到构建中
图 2 简要介绍了一个配置文件。配置文件由目标树构成。每个目标都包含了要执行的任务,其中任务就是可以执行的代码。在本例中,mkdir 是目标 compile 的任务。mkdir 是建立在 Ant 中的一个任务,用于创建目录。 Ant 带有一套健全的内置任务。您也可以通过扩展 Ant 任务类来添加自己的功能。 每个目标都有唯一的名称和可选的相关性。目标相关性需要在执行目标任务列表之前执行。例如图 2 所示,在执行 compile 目标中的任务之前需要先运行 JUNIT 目标。这种类型的配置可以让您在一个配置中有多个树。 图 2. Ant XML 构建图 与经典 make 实用程序的相似性是非常显著的。这是理所当然的,因为 make 就是 make。但也要记住有一些差异:通过 Java 实现的跨平台和可扩展性,通过 XML 实现的可配置,还有开放源代码。 下载和安装 Ant 下载和安装 JUnit 定义目录结构
在实际中,我们有多个目录,例如 因为目录结构经常变动,所以在 |
Ant 构建配置文件示例
下一步,我们要创建配置文件。清单 4 显示了一个 Ant 构建文件示例。构建文件中的关键就是名为 runtests 的目标。这个目标进行分支判断并运行外部程序,其中外部程序是前面已安装的 junit.textui.TestRunner
。我们指定要使用语句 test.com.company.AllJUnitTests
来运行哪个测试套件。
清单 4. 构建文件示例
<property name="app.name" value="sample" /> <property name="build.dir" value="build/classes" /> <target name="JUNIT"> <available property="junit.present" classname="junit.framework.TestCase" /> </target> <target name="compile" depends="JUNIT"> <mkdir dir="${build.dir}"/> <javac srcdir="src/main/" destdir="${build.dir}" > <include name="**/*.java"/> </javac> </target> <target name="jar" depends="compile"> <mkdir dir="build/lib"/> <jar jarfile="build/lib/${app.name}.jar" basedir="${build.dir}" includes="com/**"/> </target> <target name="compiletests" depends="jar"> <mkdir dir="build/testcases"/> <javac srcdir="src/test" destdir="build/testcases"> <classpath> <pathelement location="build/lib/${app.name}.jar" /> <pathelement path="" /> </classpath> <include name="**/*.java"/> </javac> </target> <target name="runtests" depends="compiletests" if="junit.present"> <java fork="yes" classname="junit.textui.TestRunner" taskname="junit" failοnerrοr="true"> <arg value="test.com.company.AllJUnitTests"/> <classpath> <pathelement location="build/lib/${app.name}.jar" /> <pathelement location="build/testcases" /> <pathelement path="" /> <pathelement path="${java.class.path}" /> </classpath> </java> </target> </project> |
运行 Ant 构建示例
开发过程中的下一步是运行将创建和测试 HelloWorld 类的构建。清单 5 显示了构建的结果,其中包括了各个目标部分。最酷的那部分是 runtests 输出语句:它告诉我们整个测试套件都正确运行了。
我在图 4 和图 5 中显示了 JUnit GUI,其中所要做的就是将 runtest 目标从junit.textui.TestRunner
改为 junit.ui.TestRunner
。当您使用 JUnit 的 GUI 部分时,您必须选择退出按钮来继续构建过程。如果使用 Junit GUI 构建包,那么它将更难与大型的构建过程相集成。另外,文本输出也与构建过程更一致,并可以定向输出到一个用于主构建记录的文本文件。这对于每天晚上都要进行的构建非常合适。
清单 5. 构建输出示例
E:/projects/sample>ant runtests Searching for build.xml ... Buildfile: E:/projects/sample/build.xml JUNIT: compile: [mkdir] Created dir: E:/projects/sample/build/classes [javac] Compiling 1 source file to E:/projects/sample/build/classes jar: [mkdir] Created dir: E:/projects/sample/build/lib [jar] Building jar: E:/projects/sample/build/lib/sample.jar compiletests: [mkdir] Created dir: E:/projects/sample/build/testcases [javac] Compiling 3 source files to E:/projects/sample/build/testcases runtests: [junit] .. [junit] Time: 0.031 [junit] [junit] OK (2 tests) [junit] BUILD SUCCESSFUL Total time: 1 second |
了解测试的工作原理
让我们搞点破坏,然后看看会发生什么事。夜深了,我们决定把 "Hello World" 变成一个静态字符串。在更改期间,我们不小心打错了字母,将 "o" 变成了 "0",如清单 6 所示。
清单 6. Hello world 类更改
package com.company; public class HelloWorld { private final static String HELLO_WORLD = "Hell0 World"; public String sayHello() { return HELLO_WORLD; } } |
在构建包时,我们看到了错误。清单 7 显示了 runtest 中的错误。它显示了失败的测试类和测试方法,并说明了为什么会失败。我们返回到代码中,改正错误后离开。
清单 7. 构建错误示例
E:/projects/sample>ant runtests Searching for build.xml ... Buildfile: E:/projects/sample/build.xml JUNIT: compile: jar: compiletests: runtests: [junit] ..F [junit] Time: 0 [junit] [junit] FAILURES!!! [junit] Test Results: [junit] Run: 2 Failures: 1 Errors: 0 [junit] There was 1 failure: [junit] 1) testSayHello(test.com.company.HelloWorldTest) "expected:<Hello World> but was:<Hell0 World>" [junit] BUILD FAILED E:/projects/sample/build.xml:35: Java returned: -1 Total time: 0 seconds |
并非完全无痛
新的过程并不是完全无痛的。为使单元测试成为开发的一部分,您必须采取以下几个步骤:
- 下载和安装 JUnit。
- 下载和安装 Ant。
- 为构建创建单独的结构。
- 实现与主类分开的测试类。
- 学习 Ant 构建过程。
但好处远远超过了痛苦。通过使单元测试成为开发过程的一部分,您可以:
- 自动验证以捕捉更改“臭虫”
- 从接口角度设计类
- 提供干净的示例
- 在发行包中避免代码混乱和类膨胀。
实现 24x7
保证产品的质量要花费很多钱,但如果质量有缺陷,花费的钱就更多。如何才能使所花的钱获得最大价值,来保证产品质量呢?
- 评审设计和代码。 评审可以达到的效果是单纯测试的一半。
- 通过单元测试来确认模块可以使用。
尽管测试早就存在,但随着开发实践的不断发展,单元测试逐渐成为日常开发过程的一个部分。
在我 10 年的开发生涯里,为 emageon.com 工作是最重要的部分之一。在 emageon.com 时,设计评审、代码评审和单元测试是每天都要做的事。这种日常开发习惯造就了最高质量的产品。软件在客户地点第一年的当机次数为零,是一个真正的 24x7 产品。单元测试就象刷牙:您不一定要做,但如果做了,生活质量就更好。
参考资料
- 下载在本文中引用的示例代码。
- 从 Apache 网站下载 Ant。如需 Ant 文档、FAQ 和其他下载,请访问 Jakarta 项目的 Ant 主页。
- JUnit 主页提供了额外的测试示例、文档、文章和 FAQ。您可以从 www.xprogramming.com 下载 JUnit 3.2。
- Kent Beck 所写的“简单的 Smalltalk 测试”(Simple Smalltalk Testing) 讨论了一个简单的测试策略和支持它的框架。
- 请参阅其它开发者的有关单元测试的评论 (comments on unit testing)。
- 要了解其它有用的开发习惯,请访问终极编程主页 (Extreme Programming Home page)。