posts - 11, comments - 3, trackbacks - 0, articles - 0
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

(转贴)用 Jester 对测试进行测试

Posted on 2006-07-19 15:33 eddy liao 阅读(385) 评论(0)  编辑  收藏 所属分类: 软件质量
http://www-128.ibm.com/developerworks/cn/java/j-jester/

用 Jester 对测试进行测试

测试套件有缺陷,这不是玩笑

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项


最新推荐

Java 应用开发源动力 - 下载免费软件,快速启动开发


级别: 初级

Elliotte Rusty Harold , 副教授, Polytechnic University

2005 年 6 月 02 日

全面的单元测试套件对健壮的程序是必不可少的。但是如何才能保证测试套件测试了应当测试的每件事呢?Ivan Moore 的 JUnit 测试的测试器 Jester,擅长发现测试套件的问题,并提供对代码基本结构的深入观察。Elliotte Rusty Harold 介绍了 Jester 并展示如何使用它才能得到最佳结果。

测试先行的开发是极限编程(XP)中争议最少、采用最广泛的部分。到目前为止,大多数专业 Java 程序员都可能捕捉过测试 bug。(请参阅 参考资料 获得有关“被测试传染”的更多信息。) JUnit 是 Java 社区事实上的标准测试框架,没有经过全面的 JUnit 测试套件测试过的系统是不完整的。如果您的项目有全面的测试套件,那么恭喜您:您将制作出质量良好的、有利于工作的软件。但是大多数代码基础相当复杂。您能确定每个方法都被测试到、每个分支都进入过么?如果不能,那么当这些方法和分支在生产中执行的时候,应用程序会如何表现呢?

代码覆盖

对代码进行测试的下一步是用 代码覆盖 工具对测试进行度量。代码覆盖是一种查看一套测试覆盖了多少代码的方法。信心的获得,不仅需要知道测试了程序整体,还要知道每个方法在全部可能情况下都得到测试。传统情况下,这类度量的执行方法是在测试执行时对测试进行监视,可以通过 Java 虚拟机调试接口(JVMDI)或 Java 虚拟机工具接口 (JVMTI)进行,或者直接处理字节码。一次都没有执行过的语句是测试不到的。

Clover 和 EMMA(参阅 参考资料) 这类工具采用的这种方法对于发现测试不到的语句很有价值 —— 但是还不够。知道测试套件没有执行某个语句,可以证明该语句没测试到。但是,反过来不成立。如果执行了某一行代码,并不一定代表它得到测试。完全有可能存在这样的情况:测试并没有检查代码行是否生成正确结果。

当然,没有人会编写测试套件对每个语句的结果都进行验证。在众多的问题当中,这个问题可能会破坏封装。您可能认为,针对特定输入,只有方法中的每一行都操作正确,方法才会生成预期结果。但是这个假设并不合理。例如,如果没有测试到所有可能输入,也就没有测试到为处理边际情况而设计的代码,这时会如何呢?有可能还会测试到每行代码,但有可能遗漏真正的 bug。

并不简单

Jester 的方法并不简单。这个工具有可能会报告大量假阳性。例如,它可能把 System.out.println("Copyright 2005 Elliotte Rusty Harold") 语句改成 System.out.println("Copyright 3005 Elliotte Rusty Harold") ,然后报告没有破坏发生。但是,假阳性一般很容易过滤出来。另外,通常也有合适的理由怀疑像这个示例一样的情况是否真的是假阳性。例如,对于版权日期 3005 是否是测试套件应当通知的 bug,有人可能会有异议。





回页首


Jester 简介

这正是 Jester 发挥作用的地方。与 Clover 这类传统的代码覆盖工具不同,Jester 不去查看报告了哪行代码。相反,Jester 会修改源代码、重新编译源代码,然后运行测试套件,查看是否有什么事出错。例如,它会把 1 改成 2,或者把 if (x > y) 改成 if (false)。如果测试套件的关注不够,没有注意到修改,那么就说明遗漏了某项测试。

我将通过在开源的 Jaxen XPath 工具(参阅 参考资料)上应用 Jester 而对它进行演示。Jaxen 有一个基于 JUnit 的测试套件,而且这个套件的代码覆盖并不完善。

入门

在运行 Jester 之前,所有对没有修改的源代码的单元测试都必须测试通过。如果不是这样,那么 Jester 就无法知道是不是它的修改造成了破坏。(为了演示,我不得不修复一个 bug,我过去为它编写了测试用例,但是没有跟踪修复它。)

Jester 与 IDE 的集成不是特别好(或者根本不好),所以要让测试通过,重要的是正确设置 CLASSPATH 和目录。运行测试套件所需要的命令行对于每个项目都是不同的。因为 Jaxen 测试使用指向特定测试文件的相对 URL ,所以它的测试必须在 jaxen 目录中运行。下面是我最后运行 Jaxen 测试的方式:

												
														$ java -classpath ../jester136/jester.jar:target/lib/junit-3.8.1.jar
:target/lib/dom4j-core-1.4-dev-8.jar:target/lib/jdom-b10.jar
:target/lib/xom-1.0d21.jar:target/test-classes:target/classes 
junit.textui.TestRunner org.jaxen.JaxenTests
												
										

在运行 Jester 之前,还需要清楚针对测试套件的一项附加限制。除非测试失败,否则不能打印有关 System.err 的任何内容。Jester 要通过检查打印的内容来判断测试是否成功,所以对 System.err 的程序输出会把 Jester 弄混。

测试套件运行无误之后,请做一份源代码树的拷贝。记住,Jester 要向代码故意加入 bug,所以您可不要冒险在出现问题的情况下遗漏一个 bug。(如果您在使用源代码控制,那么这不会是个大问题。如果没有,请暂停阅读本文,立即把代码签入 CVS 或 Subversion 仓库。)

运行 Jester

要运行 Jester,在路径中必须同时拥有 jester.jar 和 junit.jar(JUnit 没有和 Jester 绑在一起。需要分别下载)。Jester 在类路径中查找它的配置文件,所以必须还要把 Jester 的主目录放在类路径中。当然,还需要添加所测试的应用程序需要的其他 JAR。主类是 jester.TestTester。传递给这个程序的参数是测试应用程序的测试套件名称。(我不得不为 Jaxen 编写一个主类,因为它没有包含一个可以运行它的全部测试的类。)如果把全部必要的 JAR 文件和目录都添加到 CLASSPATH 环境变量,而不是把它们添加到 jre/lib/ext 或者用 -classpath 引用它们,那么 Jester 工作起来会更加稳定。下面是我针对 Jaxen 运行初始测试的方式:

												
														$ export CLASSPATH=src2/java/main:../jester136/jester.jar:../jester136
:target/lib/junit-3.8.1.jar:target/lib/dom4j-core-1.4-dev-8.jar
:target/lib/jdom-b10.jar:target/lib/jdom-b10.jar:target/lib/xom-1.0d21.jar
:target/test-classes:target/classes
$ java  jester.TestTester org.jaxen.JaxenTests src2/java/main 
												
										

Jester 运行很慢,即使检测一个文件也是如此。它显示一个进度对话框,如图 1 所示,并在 System.out 上打印输出,让您知道它在做的工作,并向您保证它并没有完全挂起。


图 1. Jester 进度

如果在第一次运行若干分钟(或者时间足够运行完整的测试套件,甚至更长)之后,什么输出也没有看到,那么 Jester 可能 确实 挂起了,这很可能是因为类路径的问题。如果每件事都进行顺利,那么应当看到像清单 1 所示的输出:


清单 1. Jester 输出
												
																		Use classpath: src2/java/main:../jester136/jester.jar
:../jester136:target/lib/junit-3.8.1.jar:target/lib/dom4j-core-1.4-dev-8.jar
:target/lib/jdom-b10.jar:target/lib/jdom-b10.jar:target/lib/xom-1.0d21.jar
:target/test-classes:target/classes
...
src2/java/main/org/jaxen/BaseXPath.java 
 - changed source on line 192 (char index=7757) from 1 to 2
             answer.size() == ?1 )
        {
            Object first = answ

src2/java/main/org/jaxen/BaseXPath.java 
 - changed source on line 691 (char index=24848) from 0 to 1


        return results.get( ?0 );
    }
}

lots more output...
src2/java/main/org/jaxen/BaseXPath.java 
 - changed source on line 691 (char index=24848) from 0 to 1


        return results.get( ?0 );
    }
}



10 mutations survived out of 11 changes. Score = 10
took 1 minutes
												
										

从清单 1 中可以看到,BaseXPath 没有得到很好的测试。Jester 对类进行了 11 项修改,而只有一项造成测试失败。有些修改是假阳性,但是 11 处修改肯定不应当只报告 1 处。

下一步是在不破坏测试套件的情况下查看 Jester 改变的代码,看看是否需要为它编写测试。Jester 在 GUI 中显示它进行的修改,如 图 1 所示(它不能在无人控制的情况下运行,这有点烦人),在控制台上打印输出,如 清单 1 所示,并生成 XML 文件,文件中是没有产生影响的修改列表,如清单 2 所示:


清单 2. Jester XML 输出
												
																		<JesterReport>
<JestedFile fileName="src2/java/main/org/jaxen/BaseXPath.java" absolutePathFileName=
"/Users/elharo/Documents/articles/jester/jaxen/src2/java/main/org/jaxen/BaseXPath.java" 
numberOfChangesThatDidNotCauseTestsToFail="8" numberOfChanges="11" score="28">
<ChangeThatDidNotCauseTestsToFail index="7691" from="if (" to="if (true ||"/>
<ChangeThatDidNotCauseTestsToFail index="7691" from="if (" to="if (false &&"/>
<ChangeThatDidNotCauseTestsToFail index="7703" from="!=" to="=="/>
<ChangeThatDidNotCauseTestsToFail index="7754" from="==" to="!="/>
<ChangeThatDidNotCauseTestsToFail index="7757" from="1" to="2"/>
<ChangeThatDidNotCauseTestsToFail index="7826" from="if (" to="if (true ||"/>
<ChangeThatDidNotCauseTestsToFail index="7826" from="if (" to="if (false &&"/>
<ChangeThatDidNotCauseTestsToFail index="24749" from="if (" to="if (false &&"/>
</JestedFile></JesterReport>
												
										

Jester 的行号报告通常不是个好方法,所以最好是在控制台输出中查找修改的代码。下面是 清单 1 的报告中的修改:

												
														src2/java/main/org/jaxen/BaseXPath.java 
 - changed source on line 691 (char index=24848) from 0 to 1


        return results.get( ?0 );
    }
}
												
										

在这个方法中,这个修改是在类的结束处:

												
														protected Object selectSingleNodeForContext(Context context) throws JaxenException 
{
  List results = selectNodesForContext( context );

  if ( results.isEmpty() )
  {
    return null;
  }

        return results.get( 0 );
}
												
										

对测试套件迅速查找之后发现,实际上没有测试调用 selectSingleNodeForContext。所以下一步就是为这个方法编写一个测试。这个方法是 protected 的方法,所以测试不能直接调用它。有时需要编写一个子类(通常作为内部类)来测试 protected 的方法。但是在这个例子中,稍做一点检查就很快发现这个方法由同一个类中的两个 public 方法(stringValuenumberValue)直接调用。所以也可以用这两个方法来测试它:

												
														    public void testSelectSingleNodeForContext() throws JaxenException {
        
        BaseXPath xpath = new BaseXPath("1 + 2");
        
        String stringValue = xpath.stringValueOf(xpath);
        assertEquals("3", stringValue);
        
        Number numberValue = xpath.numberValueOf(xpath);
        assertEquals(3, numberValue.doubleValue(), 0.00001);
        
    }
												
										

最后一步是运行测试用例,确定它通过。下面是结果:

												
														java.lang.NullPointerException
	at org.jaxen.function.StringFunction.evaluate(StringFunction.java:121)
	at org.jaxen.BaseXPath.stringValueOf(BaseXPath.java:295)
	at org.jaxen.BaseXPathTest.testSelectSingleNodeForContext(BaseXPathTest.java:23)
												
										

Jester 捕捉到一个 bug!方法没有像预期的那样工作。更有趣的是,对 bug 的调查揭示出潜在的设计缺陷。BaseXPath 类可能更适合作为抽象类而不是具体类。我发誓,我并不是特意挑选这个示例来公开这个 bug。我从 BaseXPath 开始只是因为它是顶级 org.jaxen 包的第一个类,而且我选择 selectSingleNodeForContext 作为所测试的方法也只是因为它是 Jester 报告的最后一个错误。我真的认为这个方法没有什么问题,但是我错了。如果某些事没有经过测试,那么就应当假设它是有问题的。Jester 会告诉您出了什么问题。

下一步显而易见:修复 bug。(请确保同时对 Jester 正在处理的源树拷贝和实际树中的 bug 进行了修复。)然后,迭代 —— 针对这个类重新运行 Jester,直到任何修改都不能通过,或者可以通过的修改都是不相关的。在我为这个 bug 添加测试(并修复)之后,Jester 就报告 11 个修改中只有 8 个没有检测到,如 清单 2 所示。这在调试中是经常出现的事:修复了一个问题就修复(或者暴露了)另外几个。





回页首


Jester 的性能

因为 Jester 重新编译代码基,而且要为自己做的每个修改都重新运行测试套件,所以它的运行要比 Clover 这样的传统工具慢得多。因此,对性能加以关注是很重要的。可以用许多技术加快 Jester 的运行。

首先,如果编译在 Jester 执行时间中占了显著部分,那么请尝试使用一个更快的编译器。许多用户都报告采用 Jikes 代替 Javac 后速度有显著提高(参阅 参考资料)。可以在 Jester 主目录中的 jester.cfg 文件中修改 Jester 使用的编译命令。

第二,剖析和优化测试套件。一般情况下,人们对单元测试运行的速度没太注意,但是如果乘上 Jester 上千次执行测试套件的次数,那么任何节约都会非常显著。具体来说,要在测试套件中查找在正常代码中不会出现的问题。JUnit 会重新初始化每个执行方法的全部字段,所以如果不是测试类的每个方法都用的字段,那么把测试数据从字段中拿出来放在本地变量中,可以显著提高速度。如果形成的代码副本不合您的风格,请尝试把测试套件分成更小、更模块化的类,以便所有的初始数据可以在全部测试方法之间共享。

第三,重新组织测试套件的 suite 方法,以便最脆弱的测试(修改之后最有可能出错的)在不太脆弱的测试之前运行。只要 Jester 发现一个测试失败,就会终止运行,所以尽早失败可以短路大量耗时的额外测试。

第四,出于相似的原因,当测试失败的机会差不多时,把最快的测试放在第一位。按照大概的执行时间给测试排序。只在内存中执行的测试在访问磁盘的测试之前,访问磁盘的测试在访问 LAN 的测试之前,访问 LAN 的测试在访问 Internet 的测试之前。如果有些测试特别慢,试试去掉它们,即便这会增加假阳性的数量。在 XOM (一个用 Java 语言处理 XML 的 API)的测试套件中,在 50 个测试类中,只有很少的几个就占据了 90% 以上的执行时间。在测试的时候清除这些类可以带来 10 倍的性能提升。

最后,也是最重要的,就是不要一次测试整个代码基。每次把测试限制在一个类上,而且只运行能够暴露这个类的覆盖不足的测试。可能需要更长时间来测试每个类,但是用这种方法,几乎可以立即填补不足、修复 bug,而不必为 Jester 的一次运行完成等上好几天。





回页首


结束语

Jester 是聪明的程序员的工具包中一个重要的附加。它可以发现其他工具不能发现的代码覆盖不足,这会直接变成发现和修复 bug。使用 Jester 对代码基进行测试,可以制造出更强壮的软件。





回页首


参考资料





回页首


关于作者

Elliotte Rusty Harold 来自新奥尔良,现在他还定期返回新奥尔良研究一盆秋葵。但是,他和妻子 Beth 及他们的猫咪 Charm(以夸克命名)和 Marjorie(以他的岳母为名),定居在布鲁克林附近的 Prospect Heights。他是 Polytechnic 大学计算机科学的副教授,他在该校讲授 Java 和面向对象编程。他的 Web 站点 Cafe au Lait 已经成为 Internet 上最流行的独立 Java 站点之一,他的分站点 Cafe con Leche 已经成为最流行的 XML 站点之一。他的书包括 Effective XMLProcessing XML with JavaJava Network ProgrammingThe XML 1.1 Bible。他目前在开发处理 XML 的 XOM API 和 XQuisitor GUI 查询工具。


只有注册用户登录后才能发表评论。


网站导航: