要使测试自动化,您需要一个测试框架。您可以自己开发或购买,也可以使用某些开放源代码工具,例如 JUnit。我选择 JUnit 出于以下几个原因:
- 不需要编写自己的框架。
- 它是开放源代码,因此不需要购买框架。
- 开放源代码社区中的其他开发者会使用它,因此可以找到许多示例。
- 它可以让我将测试代码与产品代码分开。
- 它易于集成到我的构建过程中。
测试布局
图 1 显示了使用样本 TestSuite 的 JUnit TestSuite 布局。每个测试都由若干单独的测试案例构成。每个测试案例都是一个单独的类,它扩展了 TestClass 类并包含了我的测试代码,即那些曾在 main()
中出现的代码。在该例中,我向 TestSuite 添加了两个测试:一个是 SkeletonTest,我将它用作所有新类和 HelloWorld 类的起点。
图 1. TestSuite 布局
测试类 HelloWorldTest.java
按照约定,测试类的名称中包含我所测试的类的名称,但将 Test 附加到结尾。在本例中,我们的测试类是 HelloWorldTest.java
。我复制了 SkeletonTest 中的代码,并添加了 testSayHello()
来测试 sayHello()
。请注意 HelloWorldTest 扩展了 TestCase。JUnit 框架提供了 assert
和 assertEquals
方法,我们可以使用这些方法来进行验证。 HelloWorldTest.java
显示在清单 2 中。
清单 2. HelloWorldTest.java
package test.com.company;import com.company.HelloWorld; import junit.work.TestCase;import junit.work.AssertionFailedError;/** * JUnit 3.2 testcases for HelloWorld */ public class HelloWorldTest extends TestCase { public HelloWorldTest(String name) { super(name); } public static void main(String args[]) { junit.textui.TestRunner.run(HelloWorldTest.class); } public void testSayHello() { HelloWorld world = new HelloWorld(); assert( world!=null ); assertEquals("Hello World", world.sayHello() ); }}
|
testSayHello()
看上去和 HelloWorld.java
中原来的 main 方法类似,但有一个主要的不同之处。它不是执行 System.out.println
并显示结果,而是添加了一个 assertEquals()
方法。如果两个值不同, assertEquals
将打印出两个输入的值。您可能已经注意到这个方法不起作用!HelloWorld 中的 sayHello()
方法不返回字符串。如果我先写过测试,就会捕捉到这一点。我将 "Hello World" 字符串与输出流联结起来。这样,按照清单 3 中显示的那样重写了 HelloWorld,去掉 main()
,并更改了 sayHello()
的返回类型。
清单 3. Hello world 测试案例。
package com.company;public class HelloWorld { public String sayHello() { return "Hello World"; }}
|
如果我保留了 main()
并修改了联系,代码看上去如下:
public static void main( String[] args ) { HelloWorld world = new HelloWorld(); System.out.println(world.sayHello()); }
|
新的 main()
与我测试程序中的 testSayHello()
非常相似。是的,它看上去不象是一个现实世界中的问题(这是人为示例的问题),但它说明了问题。在单独的应用程序中编写 main()
可以改进您的设计,同时帮助您设计测试。现在我们已经创建了一个测试类,让我们使用 Ant 来将它集成到构建中。
使用 Ant 将测试集成到构建中
Jakarta Project 将 Ant 工具说成“不带 make 缺点的 make”。Ant
正在成为开放源代码世界中实际上的标准。原因很简单:Ant 是使用 Java
语言编写的,这种语言可以让构建过程在多种平台上使用。这种特性简化了在不同 OS
平台之间的程序员的合作,而合作是开放源代码社区的一种需要。您可以在自己选择的平台上进行开发 和构建。Ant 的特性包括:
- 类可扩展性 Java 类可用于扩展构建特性,而不必使用基于 shell 的命令。
- 开放源代码 因为 Ant 是开放源代码,因此类扩展示例很充足。我发现通过示例来学习非常棒。
- XML 可配置 Ant 不仅是基于 Java 的,它还使用 XML 文件配置构建过程。假设构建实际上是分层的,那么使用 XML 描述 make 过程就是其逻辑层。另外,如果您了解 XML,要学习如何配置构建就更简单一些。
图 2 简要介绍了一个配置文件。配置文件由目标树构成。每个目标都包含了要执行的任务,其中任务就是可以执行的代码。在本例中, mkdir是目标 compile的任务。 mkdir是建立在 Ant 中的一个任务,用于创建目录。 Ant 带有一套健全的内置任务。您也可以通过扩展 Ant 任务类来添加自己的功能。
每个目标都有唯一的名称和可选的相关性。目标相关性需要在执行目标任务列表之前执行。例如图 2 所示,在执行 compile 目标中的任务之前需要先运行 JUNIT 目标。这种类型的配置可以让您在一个配置中有多个树。
图 2. Ant XML 构建图
与经典 make 实用程序的相似性是非常显著的。这是理所当然的,因为 make 就是 make。但也要记住有一些差异:通过 Java 实现的跨平台和可扩展性,通过 XML 实现的可配置,还有开放源代码。
下载和安装 Ant
首先下载 Ant(请参阅 参考资料 )。将 Ant 解压缩到 tools 目录,再将 Ant bin
目录添加到路径中。(在我的机器上是 e:"tools"ant"bin
。)设置 ANT_HOME 环境变量。在 NT 中,这意味着进入系统属性,然后以带有值的变量形式添加 ANT_HOME。ANT_HOME 应该设置为 Ant 根目录,即包含 bin
和 lib
目录的目录。(对我来说,是 e:"tools"ant
。)确保 JAVA_HOME 环境变量设置为安装了 JDK 的目录。Ant 文档有关于安装的详细信息。
下载和安装 JUnit
下载 JUnit 3.2(请参阅 参考资料 )。解开 junit.zip
,并将 junit.jar
添加到 CLASSPATH。如果将 junit.zip
解包到类路径中,可以通过运行以下命令来测试安装: java junit.textui.TestRunner junit.samples.AllTests
定义目录结构
在开始我们的构建和测试过程之前,需要一个项目布局。图 3 显示了我的样本项目的布局。下面描述了布局的目录结构:
build
-- 类文件的临时构建位置。构建过程将创建这个目录。
src
-- 源代码的位置。 Src
被分为 test
文件夹和 main
文件夹,前者用于所有的测试代码,而后者包含可交付的代码。将测试代码与主要代码分离提供了几点特性。首先,使主要代码中的混乱减少。其次,它允许包对
齐。我就热衷与将类和与其相关的包放置在一起。测试就应该和测试在一起。它还有助于分发过程,因为你不可能打算将单元测试分发给客户。
在实际中,我们有多个目录,例如 distribution
和 documentation
。我们还会在 main
下有多个用于包的目录,例如 com.company.util
。
因为目录结构经常变动,所以在 build.xml
中有这些变动的全局字符串常数是很重要的。
图 3. 项目布局图
Ant 构建配置文件示例
下一步,我们要创建配置文件。清单 4 显示了一个 Ant 构建文件示例。构建文件中的关键就是名为 runtests 的目标。这个目标进行分支判断并运行外部程序,其中外部程序是前面已安装的 junit.textui.TestRunner
。我们指定要使用语句 test.com.company.AllJUnitTests
来运行哪个测试套件。
清单 4. 构建文件示例
<property name="app.name" ="sample" /> <property name="build.dir" ="build/classes" /> <target name="JUNIT"> <available property="junit.present" classname="junit.work.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="true"> <arg ="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 runtestsSearching for build.xml ... Build E:"projects"sample"build.xmlJUNIT:compile: [mkdir] Created dir: E:"projects"sample"build"classes [javac] Compiling 1 source file to E:"projects"sample"build"classesjar: [mkdir] Created dir: E:"projects"sample"build"lib [jar] Building jar: E:"projects"sample"build"lib"sample.jarcompiletests: [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 SUCCESSFULTotal time: 1 second
|
图 4. JUnit GUI 测试成功
图 5. JUnit GUI 测试失败
了解测试的工作原理
让我们搞点破坏,然后看看会发生什么事。夜深了,我们决定把 "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 runtestsSearching for build.xml ... Build E:"projects"sample"build.xmlJUNIT: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: -1Total time: 0 seconds
|