使用 Grester 简化 Java 应用程序的 JUnit 测试
Jester 由 Ivan Moore 编写,它是测试由程序员和开发人员编写的单元测试的优秀工具。该工具基于这样一个假设:代码中的很多区域包含了条件语句、循环语句和 case 语句,并且在一些代码区域中,类的圈复杂度(cyclomatic complexity)由于存在很多执行路径而会突然增加。Jester 主要关注的就是类似这样的代码。但是要运行它,必须使用指向各个资源的格式良好的类路径。
Grester 是针对 Jester 的 Apache Maven 包装器,它减轻了从项目依赖关系构造 Java™ 类路径之类的麻烦,这样您可以使用 Jester 更轻松地测试执行点。Grester 还增强了使用 Maven 的一些优点,Maven 是其基础架构的核心。在对没有使用测试驱动方式编写的代码进行额外检查时,Jester 尤为有用。此类代码可以是旧式应用程序中的遗留代码,甚至还可以是最近编写的代码,这些代码的开发人员认为使用 Agile 的测试驱动方法指导构建高质量代码来说难度较大。
实际上,您可以使用 Grester 来突破使用非测试驱动方式编写代码的限制。根据我的经验,范围蔓延(scope creep)和可能错失或绕过实际业务函数的代码将会增加错误数和恶意代码 数量(快速处理 blob 反模式,即使在很小的代码段中,都不必作为单个难于管理的模块或模块集)。
本文不会探讨解释 Jester 的输出及精确说明 Jester 如何工作的技术细节。要获得这些信息,请参阅 参考资料 中由 Elliott Rusty Harold 撰写的优秀文章,或者访问 Ivan Moore 的 Web 站点。本文旨在探讨获取并围绕 Jester 使用 Maven 插件包装器。
您可以通过两个来源之一获取 Grester,它们都列在 参考资料 中。运行 Grester 所需的基础设施非常少:您只需要使用 Maven 就能构建和使用 Grester。Grester 是用 Groovy 编写的,Groovy 是一种动态语言,它的语法类似于 Java 并且拥有 Python 和 Ruby 等语言的优点。Grester 本质上只是用于快速运行 Jester 工具的另一个 Maven 插件,因此 Grester 的真正威力来自 Jester。在本文中,结合使用了 Jester V1.37 与 Grester V0.3 alpha 发行版。
如果所有项目 Java Archive (JAR) 依赖关系都位于一个位置,那么直接运行 Jester 不可能比在 Java 类路径条目中引用单个路径更简单。但是,当依赖关系散布到整个文件系统中时,每次 Jester 运行的配置问题会十分复杂而讨厌,尤其是在各个依赖关系随时间改变位置时。使用 Maven 将显著简化此过程。
Jester 运行在 Maven 项目构建配置以外的每个实例中。那么,Grester 的特别之处在哪里?答案在于 Maven 组织其依赖关系的方式。这种 “安排” 非常高效,Maven 不但尝试把 Java(或 Groovy)JAR 和 Web 归档 (WAR) 的查找方式标准化,而且还尝试把保存方式标准化。
|
如果您不熟悉 Maven,请使用系统库 的概念。有一个位于 $USER_HOME/.m2/repository 的默认本地系统库,还有一个在位于 $MAVEN_HOME/conf 的 pom.xml 或 settings.xml 文件中配置的远程系统库。
获取 TAR 压缩资源(.tar 文件和 tar.gz 文件适用于 UNIX® 和 Linux®)或者 Microsoft® Windows® ZIP 文件后,请将其解压缩。有很多种方法可以完成此操作:在这里,我使用 Windows 中的 Cygwin 实用程序。
图 1. 用 Windows 中的 Cygwin 实用程序解压缩 Grester
您还可以将 TAR 实用程序与 xzvf
选项结合使用来解压缩 tar.gz 文件,或者与 xvf
选项结合使用来解压缩普通 .tar 文件。图 2 给出了该过程的示例。
图 2. 用 TAR 实用程序解压缩 Grester tar.gz 文件
最终的目录结构应当类似于图 3。
此时,您已经准备好让 Maven 知道可以从哪个外部系统库获得相关 Grester Groovy 依赖关系,从而在本地把 Grester 编译和安装成 Maven 插件。您可以通过把两个远程系统库添加到 $MAVEN_HOME/conf/settings.xml 文件中来完成此操作,如下所示:
清单 1. 把 Maven 指向包含 Groovy 依赖关系的远程系统库
<settings>
<profiles>
<profile>
<id>repositoryDefinitions</id>
<repositories>
.....
.....
<!-- You may have other repositories -->
....
....
<repository>
<id>apache-snapshotsv/id>
<name>Apache Snapshots Repository</name>
<url>http://people.apache.org/repo/m2-snapshot-repository</url>
<layout>default</layout>
<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
<checksumPolicy>ignore</checksumPolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
.....
.....
</repositories>
...
</profile>
</profiles>
</settings>
|
接下来是 Maven 的插件配置,该插件配置将指定 Grester 的 Groovy 插件依赖关系的系统库。此插件系统库配置放在为系统库声明的同一个配置文件 中(例如,名称 repositoryDefinitions 被用作配置文件的名称),如下所示:
清单 2. 把 Maven 指向包含 Groovy 插件依赖关系的远程系统库
<settings>
<profiles>
<profile>
<id>repositoryDefinitions</id>
....
....
</repositories>
<pluginRepositories>
<!-- You may have other plug-in repositories -->
....
....
<pluginRepository>
<id>apache-snapshots</id>
<name>Apache Snapshots Repository</name>
<url>http://people.apache.org/repo/m2-snapshot-repository</url>
<layout>default</layout>
<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
<checksumPolicy>ignore</checksumPolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
...
...
</pluginRepositories>
...
</profile>
</profiles>
</settings>
|
现在您终于可以构建插件直至完成。Grester 要求使用 Maven V2.0.5 或更高版本。如果使用早期版本,则会在编译和使用 Groovy-mojo-support 依赖关系中的功能时遇到问题。如果 $MAVEN_HOME/bin 目录是可执行文件系统路径的一部分,则可以从 maven-grester-plugin 目录(包含 Grester 的 pom.xml 文件的目录)中执行 mvn clean install
命令,如下所示:
构建通常运行得很快(少于 20 秒)。图 5 显示成功安装窗口。
图 5. 在 Maven 的本地系统库中安装 Grester
|
必须注意的是 Grester 安装在 Maven 的本地系统库中的位置。如果不熟悉 Maven,则其默认本地系统库为 $USER_HOME/.m2/repository/。默认情况下,在运行 Windows 的计算机中,$USER_HOME 很可能被转换为 Documents and Settings/$USERNAME/(其中 $USERNAME 是登录的用户)。在 Linux/UNIX 计算机中,$USER_HOME 将转换成 /home/$USERNAME/。快速浏览 Windows 本地系统库可以发现 Grester 被安装到 C:/Documents and Settings/$USERNAME/.m2/repository/org/apache/maven/plugins 中,并且创建了一个名为 maven-grester-plugin 的目录。此目录包含版本号目录(最新版本是 V0.3);该目录中有实际的 maven-grester-plugin-x.x.jar 文件。
使用此结构的原因在于 Grester 的 pom.xml 文件。如图 6 所示,Grester 项目的 groupId
是 org.apache.maven.plugins。用包含此字符串作为 groupId
值的 Java 或 Groovy 语言编写的所有 Maven 插件,相比拥有其他一些任意 groupId
的 Maven 插件,都包含更易于通过命令行执行的 mojo。由于 Grester 将使用此字符串,因此在通过命令行执行各个 mojo 目标时,您无需预先考虑 groupId
和 artifactId
。
图 6. Grester 的 pom.xml 配置文件中的 groupId
maven-grester-plugin 目录是在安装时创建的(install
目标将创建此目录),如下所示。其他标准 Maven 插件都安装在同一个上级目录中,例如 maven-surefire-plugin 和 maven-install-plugin 目录。
如果项目的自定义组 ID 和工件 ID 太长且很难记或者只是重复键入很麻烦,那么使用特殊的 groupId
字符串十分有利。这是默认插件(例如,maven-compiler-plugin 或 maven-surefire-plugin)的基本 Maven 目标(例如 compile
、test
,或者 test-compile
)在执行时不需要诸如 mvn org.apache.maven.plugin:maven-compiler-plugin:2.0.2:compile
或 mvn org.apache.maven.plugin:maven-surefire-plugin:2.3:test
之类的命令的原因(只需 mvn compile
或 mvn test
)。
此时,除了 Grester 的核心 —— 实际的 Jester 依赖关系之外其他内容都已就绪。Windows 和 Linux/UNIX 平台提供了两个方便的脚本,它们可以把 Jester(即,实际的 jester-1.37.jar 文件)安装到 Maven 的本地系统库中。为什么提供这些脚本?难道不能在 Maven 获得其编译器、安装程序和其他插件依赖关系时从 Maven 所在的相同外部资源中下载这些脚本么?答案是 Jester 没有放在可公开获得的已知 Maven 系统库中(例如,Maven 的 Ibiblio),因此您不能用包含 Jester 的远程系统库来配置 Maven 的 $MAVEN_HOME/conf/settings.xml 文件(不考虑用 groupId
-artifactId
版本组合安装它的方法)。
因此,分别为 Windows 和 Linux/UNIX 提供了 install-jester.bat 和 install-jester.sh 可执行文件。如果任何一个可执行文件在任意平台上执行失败,都可以使用如下所示的命令作为解决方法。
注:我写完这篇文章后,Grester V1.0.1 被发布到 Maven 公共系统库 中。这种持续不断地改进意味着您现在可以直接从著名的 Apache 系统库中获得插件,但是仍然需要有提供结合功能的 Jester 核心 JAR 和指令才能构成完整的架构。
那么,您已经得到了一个精心编织的 Maven 项目,并且希望在单元测试(或者至少在一组测试中)中测试 Jester。无论是单元测试还是集成测试,明智的做法是要么把项目复制到文件系统中的某个位置并对该副本运行 Jester,要么使用现有副本,但是要准备好恢复对代码源文件所做的所有更改。这是因为 Jester 将更改现有的源代码文件,保存更改并重新编译代码(保留同一个目录中的类文件作为源文件)。如果项目拥有的代码库相对较小或者所选测试很少,则可以使用现有代码库副本。
以测试为例,您将使用在 Eclipse IDE 中构造和准备的基本 Maven 项目。虽然 参考资料 部分包含关于如何操作的链接和信息,但是如何在特定开发环境内构造 Maven 项目和创建必要文件不在本文讨论范围内。图 9 演示了 Eclipse IDE 内的项目。
图 9. Eclipse IDE 中的示例 Maven 项目
举例来说,在项目中使用一个相对简单的类和测试类。该类将使用 Java 语言处理外部过程命令的执行。清单 3 显示了测试的类的主要部分。
package com.prometheus.run; import java.io.IOException; import java.io.InputStream; public class CommandExecutor extends Executor{ ... public String executeCommand(String command){ ... try { Process child = performCommandExecution(command); stream = child.getInputStream(); sb = processStream(stream); ... } ... return sb.toString(); } protected StringBuffer processStream(InputStream stream) throws IOException { ... sb = new StringBuffer(); while ((c = stream.read()) != -1) { sb.append((char)c); } return sb; } ... } |
在 CommandExecutor
类中,executeCommand()
方法将调用同一个类 processStream()
中的受保护方法。在 processStream()
方法中,将在 while()
循环中创建一个新 StringBuffer
实例并且处理 InputStream
。清单 4 显示了测试类,还显示了测试的主要部分。
package com.prometheus.run; import com.prometheus.run.CommandExecutor; ... public class CommandExecutorTest extends TestCase { ... public class MockProcess extends Process{ ... public InputStream getInputStream(){ String source= "This is a mock string"; return new ByteArrayInputStream(source.getBytes()); } public OutputStream getOutputStream(){ return null; } public int waitFor(){ return 1; } } public void testExecuteCommmand(){ String expected = "This is a mock string"; String actual = commandExecutor.executeCommand("lsmod"); assertEquals(expected, actual); ... } } |
测试类 CommandExecutorTest
相对简单。虽然给出的详细信息不多,但是此单元测试的基本目标是在测试时通过类的 performCommandExecution()
方法调用来模拟 Process
类的行为。
必须注意的是,要让 Grester 成功运行,项目必须编译代码源文件和测试源文件并成功运行任意一个测试和所有测试(注:由于这个原因,test-compile
Maven 阶段将标记允许 Grester 运行且不能提前运行的阶段)。下一步是简单地在项目的 pom.xml 文件中附加 Grester 的 Maven 插件配置。此配置放在 pom.xml 文件的默认构建部分中或任何常规的 Maven 配置文件中。
清单 5 显示了放在示例项目的 pom.xml 文件中的 Grester 插件的示例配置。注意,groupId
要对应于 org.apache.Maven.plugins 并且版本应该是最新的 Grester 插件:V0.3。
<plugins> ... ... <!-- START MAVEN GRESTER PLUG-IN CONFIGURATION --> <plugin> <groupId>org.apache.Maven.plugins</groupId> <artifactId>Maven-Grester-plugin</artifactId> <version>0.3</version> <configuration> <codeSources>src/main/java/com/prometheus/run</codeSources> <testSuiteClass>com.prometheus.run.CommandExecutorTest</testSuiteClass> </configuration> <executions> <execution> <id>inspectSourcesCodeWithGrester</id> <phase>test</phase> <goals> <goal>inspect</goal> </goals> </execution> </executions> </plugin> <!-- END MAVEN GRESTER PLUG-IN CONFIGURATION --> ... </plugins> |
注意,项目已被设为在 Maven 的测试阶段运行 Grester 的 inspect
目标。codeSources
将指向包含测试类 CommandExecutorTest
的源代码的目录。它可以像简单地指向实际类 CommandExecutor
一样排除文件扩展名。在 Grester 附带的 README.txt 文件中提到了扩展名 .Groovy,但是应当注意的是,目前没有对 Grester 的支持。
从 V0.3 alpha 开始,可以在所有有效的 Maven 生命周期阶段中作为插件执行的 Grester 有两个主要目标(使用时全小写):
-
inspect
—— 这是 Grester 的主要目标,通常在测试阶段(虽然严格来说,它可以是测试编译阶段之后的任意阶段)执行。Grester 将通过 pom.xml 文件中列出的依赖关系创建一个可变的 Java 类路径并把新类路径提供给 Jester。 -
help
—— 此目标主要用于对正确插件语法和结构的参考,可以在命令行中输入mvn grester:help
单独执行。
运行简单的 mvn clean install
命令(或者包含 inspect
目标使用的特定状态的所有生命周期命令)将生成如下所示的输出。
通过进一步检查,您可以看到初始类文件 CommandExecutor
中的第 27 行已经从 -1
更改为 1
。Jester 对单个类执行一个完整操作需要花费一些时间。在操作结束时将生成 jesterReport.xml 文件,该文件显示在 Java Swing 窗口中所发生情况的汇总详细信息。
通过命令行运行 mvn grester:help
将生成类似于图 11 的输出。它将用作配置 Grester 的简短指南,而无需参考初始的 README.txt 文件。
Grester 不是完美的插件,并且仍然在改进中。对 Groovy 源代码的直接支持特别有帮助。同样的概念可以应用到不使用 Maven 但需要构造 Java 类路径字符串(例如,跨越单个文件系统中的多个目录列出依赖关系的 Apache Ant 构建文件)的项目。如果 Ant 文件本身已经被分成许多独立的文件,那么该过程可能会更加复杂。
对于在一个位置中无法轻松识别其依赖关系的项目,运行单个工具 (Jester) 所带来的麻烦是否值得您去承受。但是,我仍然觉得 Jester 是用于考察开发人员编写测试的方法是否具有健壮性的重要工具。确实,对于使用一组静态测试即可找出的重大代码库更改,当 Jester 报告显示出很差的单元测试或集成测试性能时,开发人员的测试驱动开发(Test-Driven Development,TDD)和行为驱动开发(Behaviour-Driven Development,BDD)技能会让人产生怀疑。