诊断 Java 代码:单元测试与自动化代码分析协同工作

发表于:2007-05-05来源:作者:点击数: 标签:代码自动化java测试单元
单元测试 和静态分析通常被看作是有助于确保程序的正确性的互不相干的方法。本文研究了这两种方法之间的关系,并讨论了构成每种方法 工作 构架的工具如何相得益彰。特别地,Eric Allen 讨论了一些可用而又令人兴奋的新应用程序,这些应用程序允许您进一步提

单元测试和静态分析通常被看作是有助于确保程序的正确性的互不相干的方法。本文研究了这两种方法之间的关系,并讨论了构成每种方法工作构架的工具如何相得益彰。特别地,Eric Allen 讨论了一些可用而又令人兴奋的新应用程序,这些应用程序允许您进一步提升您的单元测试

这是一场古老的争论 — 哪种方法对产生健壮代码更有价值:测试还是静态分析和验证?您会在程序员的日常工作中听到这种争论,尤其是在极端编程(Extreme Programming)CC%B3');" target="_self">论坛上。(请参阅我们由 Roy Miller 主持的 XP 论坛。)

支持静态分析(包括类型检查)的主要论据是:其结果适用程序所有可能的运行,而通过单元测试只能保证被测试的组件(在测试它们的平台上)只适用测试组件的特定输入。

支持单元测试的主要论据是它更容易处理。您可以测试程序的许多约束,这些约束远远超出了同期的静态分析工具所能达到的范围。

请允许我在此冒昧地说一句:我认为将这两种工具看作对立的是一个错误。每种工具都有助于构建更健壮的程序。实际上,它们可以通过非常强大的方式进行互补。

每种工具都有各自的长处,对于补充另一种工具特别有用:

  • 单元测试能显示执行的常用路径,从而显示程序是如何运行的。
  • 分析工具能检查单元测试提供的覆盖范围。

让我们研究这其中的每个属性,并讨论一些可帮助您将其长处带给其它方法的工具。

显示常用执行路径的单元测试
单元测试套件提供了程序组件的示例用法的稳固基础。通过检查测试运行时程序是如何运作的,分析工具可以就开发人员希望在程序中保持的不变量进行试探性推测(就和程序员阅读单元测试所做的一样)。

还有另一种方法,其中单元测试可以是一种可执行的文档形式。在从单元测试的运行中从特殊到一般地推断出推测性不变量之后,分析工具可以尝试从一般到特殊地验证不变量的存在,或者它可以利用可在运行时检查的断言注释该代码。

在任何一种情况下,在该工具做任何其它工作之前,最好向用户返回推测的不变量集的报告,以询问用户真正想要哪些不变量。顺便提一下,如果此类工具向用户报告了许多他们不想要的不变量,这可能是单元测试出了问题的信号 — 例如,它们不够一般。

可用这种方式与单元测试一起使用的工具是 Daikon,它是一款来自 MIT 的 Mike Ernst 的程序分析小组的免费的、试验性的工具。Daikon 分析程序的运行(例如单元测试的运行),并尝试推测不变量。然后它询问用户是否想要这些不变量,并将用户想要的不变量作为断言插入程序。

例如,假定我们编写一个向量(Vector)的适配器,该适配器实现接口 Sequence,该接口包含用于检索元素的方法 lookup 和用于将元素放在向量末尾的方法 insert。方法 lookup 带有一个索引 i,用来访问它所包含的向量。

假定该数组的长度存储在字段 length 中。通过维护适配器中的长度,我们可以不通知向量本身就将元素从其尾部删除。

让我们为这个假想的简单适配器编写一个简单的测试用例

清单 1. 向量容器中简单查找方法的测试用例

clearcase/" target="_blank" >cccccc height=17>import junit.framework.TestCase;

public class VectorAdapterTest extends TestCase {
public VectorAdapterTest(String name) {
super(name);
}

public void testLookupAndInsert() {
VectorAdapter v = new VectorAdapter();
v.insert("this");
v.insert("is");
v.insert("a");
v.insert("test");
assertEquals("Retrieved and inserted elements don't match",
"a",
v.lookup(2));
}
}

然后我们可以实现我们的适配器以通过这个测试,如下所示:

清单 2. 类 VectorAdapter

import java.util.Vector;

public class VectorAdapter implements Sequence {
private Vector values = new Vector();
private int length = 0;

public void insert(Object o) {
length += 1;
values.addElement(o);
}

public Object lookup(int i) {
return values.elementAt(i);
}
}

interface Sequence {
public void insert(Object o);
public Object lookup(int i);
}

当 Daikon 在这段代码上运行时,它可能推断:对于方法 lookupi 总是小于 length。Daikon 可能从单元测试中推断出这一点,并向我们的方法报告一条前置条件:i < length

然后程序员可以检查 Daikon 报告的不变量,从而更好地了解其测试覆盖程序的范围到底怎么样。例如,如果 Daikon 开始推断出大量不想要的不变量,这意味着单元测试只是用不具代表性的可能的程序输入的子集检测了程序。

尽管 Daikon 是用 Java 语言编写的,但它需要用 C++ 编写的前端,这削弱了它原有的可移植性。尽管如此,还是可以在线获得针对许多主要平台的前端构建。此外,Daikon 团队也打算添加其它平台所需要的构建。

(您可以在参考资料一节找到关于 Daikon 的下载信息和更多内容。)

可以检查单元测试覆盖范围的分析工具
分析工具可以帮助程序员构建健壮的单元测试套件。迄今为止,完成这一工作主要有两种方法:

  • 使用静态分析以尝试自动生成单元测试套件
  • 使用静态分析来确定单元测试套件对程序功能的覆盖范围到底怎么样

目前有几种试图自动从代码产生单元测试的免费工具,但大多数担任这项任务的免费工具还处于起步阶段。其中一些比较有希望的是 JUnitDoclet 和 JUB(“JUnit test case Builder”的缩写),可在 SourceForge 上得到它们(参考资料一节提供了它们的链接)。

关于这些类型的工具,要牢记的要点是:最适宜应用于通过测试更新旧代码。当构建新项目时,它们的作用不大。

为什么会这样呢?因为新项目应该与项目上的单元测试是一前一后构建的。开发单元测试是构建设计的强有力的方法;针对组件的 API 就是在编写测试时隐式地为它们设计的。此外,以这种风格进行设计向设计师提供了即时的反馈。糟糕的设计将非常难于编写测试!并且,任何分析工具在确定为程序编写什么测试这方面,都很难做得象设计师那样好。

第二种分析工具分析程序及其单元测试,并确定测试能在多大范围内覆盖程序。与刚才提到的第一类工具不同,此类工具对每个项目都是有用的。实际上,极端编程团队可以考虑将此类工具集成到他们的代码提交过程中。那么,他们不仅能够防止代码在通过所有测试之前被提交,而且可以防止代码在未经测试的情况下提交!不仅懒惰会导致测试覆盖范围偏小,错误也可能导致同样后果,因此,此类强制措施对任何技能(和完整性)级别的程序员都有用。

Clover 是一种可以执行此类分析的新的并且特别有希望的工具。Clover 是 Ant 的插件,Ant 是 make 的流行的、全 Java 的替代物。Clover 是商业工具,但它可以免费用于开放源码项目。

Clover 分两阶段过程进行工作。首先,它在编译时检测代码。然后,在测试时将有关测试的运行信息写到用来生成报告的数据库中(通过 GUI、网页或在控制台中)。

将 Clover 集成到使用 Ant 的现有项目中很简单。这涉及调整项目的 build.xml 文件以添加几个在编译、记录测试和生成报表期间检测代码的目标。例如,假定我们有一个带构建和编译目标的 build.xml 文件。我们所必须做的全部工作是将 Clover JAR 文件放到我们的 Ant 库目录中,并如下所示扩展 build.xml 文件(Clover 用户指南中提供了这些和类似于 Ant 目标的信息;为了方便,我在这里包括了它们):

清单 3. 扩充 Ant build.xml 文件以使用 Clover

<property name="clover.initstring" value="/tmp/mycoverage.db"/>

<target name="with.clover">
<property name="build.compiler"
value="org.apache.tools.ant.taskdefs.CloverCompilerAdapter"/>
</target>

<path id="clover.classpath">
<pathelement path="<CLOVER_HOME>/lib/clover.jar"/>
<pathelement path="<CLOVER_HOME>/lib/velocity.jar"/>
</path>

<target name="clover.report">
<java classname="com.cortexeb.tools.clover.reporters.html.HtmlReporter">
<arg line="--outputdir /tmp/clover_html --showSrc --initstring
$\{clover.initstring\} --title 'My Project'"/>
<classpath refid="clover.classpath"/>
</java>
</target>

特性 clover.initstring 指定了一个文件,有关 Clover 覆盖范围的数据将写入这个文件中。目标 with.clover 用来在执行其它目标(如 compiletest)时打开 Clover。clover.report 目标用来接收累积的覆盖范围数据并生成报告。

在上面的代码中,我们将生成 HTML 报告。我们也可以生成文本报告(对于提供给脚本以确定测试的覆盖范围是否可接受非常有用)和基于 Swing 的报告。

设置 clover.classpath 是必要的,以便报告生成器目标知道到哪里找到它所需要的所有类。但是,放在类路径中的第二个 JAR 文件(velocity.jar)只有在生成 HTML 报告时才是必需的。一旦完成了上述工作,就可以用以下命令生成 Clover 报告:

$ ant with.clover compile test
$ ant clover.report

就是这么简单。请查阅流行的编码工具 JBoss 和 Ant 的在线 Clover 报告,以获取一些样本输出。(您可以在参考资料一节找到更多有关这些内容的下载信息和其它信息。)

两种方法的结合
本文中讨论的工具突出了一些可以将程序分析和单元测试一起使用的方法,以提供比单独执行任何一种方法都更有效的不变量检测。这些技术只代表了所有可能技术的冰山一角。

将来,其它工具可能会提供更强有力的单元测试能力。例如,类型推断(type-inference)引擎和优化编译器可以从现有的单元测试推断线索、UML 生成工具可以从测试构造各种图表(不仅是类图)等等。对于合并这些方法以获得更佳的代码构建和故障诊断而言,还存在着巨大的空间可以进行创造性开发和实验。

请记住每种方法的属性及其长处:

  • 单元测试能够演示程序在特定的运行期是如何运转的;还能说明执行的常用路径。

  • 分析工具能够检查所有可能运行的程序的某些特定属性。

每种方法的长处都可以用来弥补另一种方法的潜在弱点。

下一次,我们将研究增强的单元测试的另一条路径,并了解一些最新的工具,它们可用于帮助您在 GUI 上开发单元测试。

参考资料

  • 请单击文章顶部或底部的讨论参与本文的论坛

  • 了解所有关于 Daikon(用 C/C++ 和 Java 前端进行动态不变量检测的原型实现)的内容,并从 Daikon 网站下载它。

  • 请查阅 Clover user guide 以获取更多关于该工具的信息,该工具用来发现未被执行的代码部分,并确定何处的代码未经足够测试。

  • Clover 与 Ant(基于 Java 的构建工具)紧密地集成在一起,如果您还没有副本,请下载一个。

  • JUnitDoclet 是使用 XDoclet 的开放源码单元测试生成工具。

  • 单元测试是极端编程的关键实践。在 Roy Miller 的 developerWorks 专栏 Demystifying Extreme Programming 中了解关于所有极端编程实践的更多信息。您也可以从 developerWorks Demystifying Extreme Programming 论坛获得 Roy 关于您 XP 问题的解答。

  • 另一种开放源码生成工具是 JUB(JUnit 测试用例构建器),可从 SourceForge 获得。

  • 可以在 JUnit 网站上找到更多扩充单元测试的工具。

  • Nicholas Lesiecki 在其文章“Test flexibly with AspectJ and mock objects”(developerWorks,2002 年 5 月)中介绍了关于测试用例独立性的问题,并向我们展示了如何使用仿真对象和 AspectJ 来开发准确而又强壮的单元测试。

  • Erik Hatcher 在其文章“Automating the build and test process”(developerWorks,2001 年 8 月)中,向您展示了他是如何修改流行的 Ant1.3 和 JUnit 测试框架,以实现构建和测试过程的完整的、定制的自动化

  • WebSphere 开发者园地在“Application Quality Assurance:Unit Testing”中提供了一篇关于用 JUnit 进行单元测试的好文章。

  • Debugging and Unit-Testing Server-Side Web Applications”也来自 WebSphere 开发者园地,它描述了包含迭代调试和交互式单元测试的服务器端 Web 开发方案。

  • 下载 DrJava,这是一个免费的、轻量型的开放源码 Java IDE,具有集成的读-计算-打印(read-eval-print)循环、调试器和 JUnit 支持。

  • 阅读 Eric Allen 的诊断 Java 代码专栏的所有文章,包括“Designing‘testable'applications”。

  • developerWorks Java 技术专区上可找到关于 Java 技术的数百篇文章和教程。
关于作者
Eric Allen 拥有康奈尔大学(Cornell University)计算机科学和数学学士学位,并且是莱斯大学 Java 编程语言团队的博士研究生。在返回莱斯完成学位之前,Eric 是 Cycorp, Inc. 的主要 Java 软件开发人员。他还主持 JavaWorld 的 Java 初学者论坛。他的研究涉及源代码和字节码级别的 Java 语言的语义模型和静态分析工具的开发。Eric 是莱斯的 NextGen 编程语言(具有附加语言特性的 Java 语言扩展)的实验性编译器的主要开发人员,并且是 DrJava(为初学者设计的开放源码 Java IDE)的项目经理。可通过 eallen@cs.rice.edu 与 Eric 联系。

原文转自:http://www.ltesting.net

...

热门标签