开发人员有多种理由决定自动化单元测试。许多人甚至进一步发挥它,自动化这些测试的定位和执行。但是假如想要测试装具模块(test harness)像静态定义的那样运行呢?请跟随开发员 Michael Nadel,看看如何利用 Python 模拟静态定义的 JUnit TestSuite 类。
JUnit 测试框架被越来越多的开发小组所共同使用。归功于各种各样的测试装具模块,现在可以测试构成任何 Java 应用程序的几乎每一个组件。事实上,几乎整个二级市场似乎都是用围绕 Junit 建立的。包括 Cactus、jfcUnit、XMLUnit、DbUnit 和 HttpUnit 这样的装具模块都可以免费供开发人员用于测试应用程序。随着系统的复杂程度的增加,并且有这么多工具可供使用,没有什么理由不依靠单元测试。
不过,开发人员不仅仅是程序员。我们与用户交互以修复 bug 并确定需求。我们参加会议并进行电话推销。我们完成一些(有时全部)质量保证功能。既然有这么多责任,希望尽可能自动化就是自然而然的了。因为好的团队(除了其他事情外)会进行大量测试,希望自动化不同的开发过程的人经常会对这一领域进行具体研究。
自动化单元测试
有许多种自动化所有项目测试用例的定位和执行的方法。一种解决方案是联合使用 Ant 的 junit 任务与嵌入的 fileset 任务。这样就可以包括和排除特定目录中的文件(基于文件名样式)。另一种选择是使用 Eclipse 的一个功能,它可以指定所有测试所在的和执行的目录。前一种选择提供了对运行的测试进行过滤的灵活性(并且由于它是一个纯粹的无头(headless)Java 应用程序,可以运行在几乎所有地方),后一种选择可以调试“动态”包。是否可以结合这两种方式的强大和灵活性?
有了 Python 编程语言的 Java 平台实现 —— Jython,回答是响亮的“可以!”(假如不熟悉 Jython,应当在继续本文之前补充这方面知识,更多信息请参阅后面的 参考资料 )。利用 Jython 的强大和优雅,可以维护一个定位文件系统、搜索匹配某种样式的类和动态编译 JUnit TestSuite 类的脚本。这个 TestSuite 类像所有其他静态定义的类一样,可以用喜爱的调试程序轻易地调试。(在本文中使用的例子假定使用的是 Eclipse IDE,不过,我在这里描述的技术不用做很多修改就可以用于大多数其他 IDE。)
在进行任何设计决定时,必须对所做的选择和决定的影响进行权衡。在这里,为了得到调试动态生成的测试包的能力,必须增加额外的复杂性。不过,这种复杂性被 Jython 自身所减轻了:Jython 经过很好测试并得到很好的支持,并且是开放源代码的。而且,Python 越来越成为面向对象的、平台独立的编程的事实上的标准。出于这两种原因,采用 Jython 的风险很少,非凡是它提供了这样的好处:在创建和调试动态生成的 JUnit TestSuite 类方面具有无可匹敌的灵活性。
假如是否采用 Jython 是主要的考虑,那么即使不使用它也可以在解决原来的问题方面有所进展。不使用 Jython 的话,可以用一个 Java Property 文件存储一组类、目录和包,以在包中加入或者排除测试。不过,假如选择使用 Jython,就可以利用整个 Python 语言和运行时来解决选择执行哪些测试的问题。Python 脚本比 Java Property 文件灵活得多,它只受限于您的想像力。
利用 Jython 与 Java 平台的无缝集成可以创建静态定义的、然而是动态构建的 TestSuite 类。有大量关于 JUnit 的教程,不过还是看下面这两行代码作为复习。清单 1 是静态构建 TestSuite 类的一个例子(这个例子取自 JUnit: A Cook's Tour,有关它和其他 JUnit 资源的链接请参阅 参考资料):
清单 1.静态定义 TestSuite
public static Test suite() {
return new TestSuite( MoneyTest.class );
}
清单 1 表明 TestSuite 是由 Test 类的类实例组成的。这个装具模块完全利用了这一点。为了分析这个工具的代码,应从 参考资料 中下载本文的示例 JAR 文件。这个文档包含两个文件:DynamicTestSuite.java 和 getalltests.py,前者是一个用 Phthon 脚本动态生成 TestSuite 的 JUnit 测试装具模块,后者是一个搜索匹配特定样式的文件的 Python 脚本。DynamicTestSuite.java 使用 getalltests.py 构建 TestSuite。可以修改 getalltests.py 以更好地适合自己的项目的需要。
了解测试装具模块
代码是如何工作的?首先,指派 getalltests.py 获取一组要执行的 Test 类。然后,使用 Jython API 将这个列表从 Python 运行时环境中提取出来。然后使用 Java Reflection API 构建在表示 Test 类名的列表中的 String 对象的类实例。最后,用 JUnit API 将 Test 添加到 TestSuite 中。这四个库的相互配合可以实现您的目标:动态构建的 TestSuite 可以像静态定义的那样运行。
看一下清单 2 中的 JUnit suite 清单。它是一个公开 public static TestSuite suite() 方法签名的 TestCase。由 JUnit 框架调用的 suite() 方法调用 getTestSuite(), getTestSuite() 又调用 getClassNamesViaJython() 以获取一组 String 对象,其中每一个对象表示一个作为包的一部分的 TestCase 类。
清单 2. 动态定义 TestSuite
/**
* @return TestSuite A test suite containing all our tests (as found by Python script)
*/
private TestSuite getTestSuite() {
TestSuite suite = new TestSuite();
// get Iterator to class names we're going to add to our Suite
Iterator testClassNames = getClassNamesViaJython().iterator();
while( testClassNames.hasNext() ) {
String classname = testClassNames.next().toString();
try {
// constrUCt a Class object given the test case class name
Class testClass = Class.forName( classname );
// add to our suite
suite.addTestSuite( testClass );
System.out.println( "Added: " + classname );
}
catch( ClassNotFoundException e ) {
StringBuffer warning = new StringBuffer();
warning.append( "Warning: Class '" ).append( classname ).append( "' not found." );
System.out.println( warning.toString() );
}
}
return suite;
}
在开始时,要保证设置了正确的系统属性。在内部,Jython 将使用 python.home 属性来定位它所需要的文件。最终会调用 getClassNamesViaJython() 方法,在这里面会有一些奇妙的事情发生,如在清单 3 中将会看到的。
清单 3. 从 Python 运行时提取 Java 对象
/**
* Get list of tests we're going to add to our suite
* @return List A List of String objects, each representing class name of a TestCase
*/
private List getClassNamesViaJython() {
// run python script
interpreter.execfile( getPathToScript() );
// extract out Python object named PYTHON_OBJECT_NAME
PyObject allTestsASPythonObject = interpreter.get( PYTHON_OBJECT_NAME );
// convert the Python object to a String[]
String[] allTests = (String[]) allTestsAsPythonObject.__tojava__( String[].class );
// add all elements of array to a List
List testList = new ArrayList();
testList.addAll( Arrays.asList( allTests ) );
return testList;
}
首先,对 Python 文件进行判定。然后,从 Python 运行时提取出一个 PyObject。这就是得到的对象,它包含将构成测试包的所有测试用例的类名(记住 —— PyObject 是 Python 对象的 Java 运行时对应物)。然后创建具体的 List 并用 PyObject 填充它,使用 __tojava__ 指示 PyObject 将其内容转换为一个 Java String 数组。最后,将控制返回 getTestSuite(),在这里装载 Jython 标识的测试用例,并将它们添加到组合包(composite)中。
在开发环境中安装测试装具模块
现在对于测试装具模块如何工作已经有了很好的熟悉,可能迫不及待要自己试试它了。您将需要完成以下步骤以配置 Eclipse 来运行这个装具模块。(假如使用不同的 IDE,应当可以轻易地针对您的环境修改这些步骤。)
安装 Jython 2.1,假如还没安装的话。(链接请见 参考资料 )。
拷贝 getalltests.py 到主目录。
编辑 getalltests.py 第 25 行以指定到源文件的根路径,会搜索在这个位置下的所有目录中与 org 包中 *Text.java 匹配的文件名。
假如有必要,修改第 54 行以改变根包名(例如,改为 com)。
将 DynamicTestSuite.java 拷贝到源树中。
将以下 JAR 添加到 Eclipse 项目中:
junit.jar (JUnit 框架二进制文件,下载信息请参阅 JUnit 的 Web 网站)。
jython.jar(Jython 二进制文件,位于 Jython 安装目录)。
将 DynamicTestSuite 类装载到 Eclipse Java 源文件编辑器中。执行以下步骤之一:
在 Package EXPlorer 视图中选择 DynamicTestSuite,或者
按 Ctrl+Shift+T 并在 Choose Type 输入字段键入 DynamicTestSuite。
从文件菜单栏选择 Run,然后选择 Debug...。
选择 JUnit 配置。
单击 New 按钮。将会创建一个新的 JUnit 目标,DynamicTestSui