级别: 中级 Mike Kelly (Mike@MichaelDKelly.com), 顾问, Fusion Alliance Allen Stoker (Allen.Stoker@PortalWizard.com), 技术专家, RAM Division, Liberty Mutual
2005 年 4 月 21 日 组件测试不应该是一个昂贵且困难的过程。通过阅读本向导,你将学会如何高效、低代价地自动化创建和部署组件测试。
"组件测试是将面向对象软件系统分解成特定粒度单元的活动,以组件或系统其它部分状态改变以及组件反应的形式,将刺激应用到组件接口并且验证刺激的正确响应,"这是Michael Silverstein在 "Component Testing with Intelligent Test Artifacts." 中写到的。如果你曾经尝试过,你会知道组件测试是非常有益的,有助于尽早发现问题,另外,因为其代价比较高,所以测试人员想知道它是否真正值得付诸很多努力。但是,从我们的经验来看,使用正确的工具、项目环境,并理解与之相关的测试模式,组件测试就可以变得很成功有效。本文中,我们来看看使用IBM® Rational® Application Developer for WebSphere Software (IRAD) 的组件测试。IRAD辅助开发人员自动化建立、开发主机、面向目标的组件测试控制、测试存根和测试驱动。
本文面向那些熟悉组件测试基本概念,也许已使用过JUnit或其它框架,以及正在IRAD开发环境中寻找建立更强大测试方法的开发人员。谈了IRAD的一些相关内容之后,我们来讨论一下测试哪些组件。然后再看看一个在IRAD中实施Java组件测试的例子。我们会提到在建立测试过程中发现的最有用的特性,以及一些我们遇到的挫折及处理方法。最后,将谈到你如何在项目团队中影响这些类型的测试。
编者著:本文所举示例均是在 Microsoft Windows XP Home 和 Professional editions 中使用 IBM Rational Application Developer for WebSphere Software 6.0.0 版本。
Rational Application Developer的一些简单介绍
IRAD的自动化组件测试特性允许你建立、编辑、部署并自动运行Java组件、Enterprise JavaBeans™ (EJB™) 组件和Web 服务的测试。使用IRAD,你可以作以下工作:
- 使用工业标准的测试脚本。
- 为Java组件、EJB组件和Web服务建立测试项目。
- 建立并编辑测试和测试数据
- 使用存根类代替Java组件、EJB组件和Web服务的依赖类。
- 部署并运行你的测试。
- 分析测试结果。
- 运行回归测试。
组件测试通常是由开发人员来建立并执行。使用IRAD,你能够通过识别贯穿多个类的流程来为Java类或更复杂的集成测试开发简单的测试。本文中,我们将看一个使用IRAD为单个类建立测试的示例。
我需要测试哪些组件?
John D. McGregor在 "Component Testing,"这篇文章中,提出定理: "当对组件工作的抱怨大于测试所需付出的努力时,为测试选择一个组件。"John接着说到,并不是每个类都需要单独地测试--只有那些足够大、足够重要,或者足够复杂的类才满足定理的条件。
在组件测试方面,有以下一些考虑:
- 使用工业标准的JUnit测试脚本。
- 可重用组件--复用组件应当经过比单一应用组件更广范围的测试。
- 领域组件--展示显著领域概念的组件在正确性和真实性方面均应当得到测试。
- 商业组件--将会当成单个产品出售的组件不仅应当作可重用组件来测试,而且还应该是责任的潜在资源。
IRAD提供了可以帮助你决定如何优化测试中的组件的详细静态度量。分析组件架构、组件复杂性和测试覆盖率的静态度量均显示在Create Java Component Test 向导中。在向导中,你可以排序、隐藏,并显示数据,以决定最好的测试策略。
- 架构度量(依赖层、聚集、分散和外部用户)衡量关系的复杂性,比如方法调用,继承或变量使用。
- 组件复杂性度量(属性、方法、声明、嵌套层、V(g)、循环复杂度计数)衡量源代码控制流的复杂度。
- 测试覆盖度量(线和测试)指出涵盖的代码行的百分比和直接使用类的方法测试的数量,来帮助你决定代码是否需要进一步的测试。
如果你使用静态度量来帮助决定测试的内容,你会发现下面来自IRAD帮助文件的建议是很有用的:
- 集中关注那些提供最高覆盖率的测试组件。分散度量代表在类之外定义的方法或属性的使用数。这很好地标示了对覆盖率有重大影响的类。具有高分散数且没有任何存根的测试类允许你快速地测试大量代码。另一方面,那些类严格的单元测试需要大量的工作来建立许多存根。
- 我们看一下测试组件,它在功能上是至关重要的。比如聚集数(公共属性的数量加上公共方法的数量),或者外部用户(确定了使用类的属性或方法的外部组件的数量)。如果对类作一些更改,则更改的值越高,回归的危险就越大。因此这些类应当得到精心的测试。
- 现在观注最复杂的组件。复杂度指示器主要是圈复杂数V和代码中的声明语句数。通常,V在1和10之间变化,值1表示代码没有分支。
- 即使你分别测试了每个类的所有方法,确保定义类-级的测试。这并不意味着你需要测试每一个类。例如,你有一些联系紧密的类,你可以考虑同时测试三个到十个类。
- 识别应该作为一个单元来测试的子系统或者是大的簇。当一个子系统满足以下任一标准时应当作为单元测试 (1) 拥有需要传给另一个开发人员的相互独立的类。 (2) 有相互作用的类和在类-级测试过程中存根的其他类。
- 使用层识别器来评价类的依赖级别。
一旦你完成了最开始一系列的测试,应用到组件(测试)的覆盖率和测试的数量允许你识别先前测试过程中还未有效识别的任何组件。
一个简单的例子
现在,让我们建立一个测试场景的简单的类,来开始我们的示例。建立一个Java 项目,然后建立一个包,命名为example,接着建立一个如下所示的类:
package example;
public class TestableExample {
public String getDataValue(String data) {
return(data);
}
}
|
该类将接收任何字符串并返回。这允许我们轻松地测试好的和差的测试用例,并看到结果。
创建一个组件测试项目
既然需要进行测试,那么,让我们通过建立一个组件测试项目来建立测试用例。该项目将使用我们的测试工件。
- 打开Navigator 视图并右击视图面板。
- 选择 New > Project,然后检查 "Show All Wizards" 并选择 Component Test > Component Test Project。
- 如果你接收到Confirm Enablement对话框,单击 OK 使用组件测试能力。
- 在Create Component Test Project页输入项目名,然后单击 Next。
- 选择Java项目作为测试的内容并单击 Finish。
- 当你改变到Test perspective,单击 Yes。
当你建立组件测试项目,Java构建路径(如图1所示,可以在项目属性中找到)将包括组件测试库。这些库包括JUnit和IBM组件测试支持JAR文件。如果你在运行这些例子时出现路径错误,请核查这些库的配置是否合适。
图1.项目属性窗口,显示Java构建路径
建立一个新的组件测试
在我们刚刚建立的项目中,建立一个新的组件测试。
- 在Test Navigator pane中右击,并选择 New > Component Test > Java Component Test.
- 单击 Next ,确认组件测试项目的目标。显示前面所提及的Create Java Component Test 向导。(请注意,在截图中显示了所有可用的度量选项。默认时,你不会看到这么多列。你可以通过单击 Options... 来更改设置,然后选择你需要的项)
Create Java Component Test 向导
- 在Create Java Component Test向导中,选择我们所建立的TestableExample类,然后单击 Next。在此选定的任何组件将包括在生成的测试用例中。
- 选择 "Method-level testing" 并单击 Next。
- 选择我们的样例类。将会为选定的所有方法构建测试用例。
- 单击 Finish。总览屏幕将显示出来。
在IRAD中,测试用例的源代码被称为行为(behavior)。总览屏幕包括左下角可以转向源代码的链接。你也可以随时在Test Navigator 面板中右击一个测试,并选择 Open Behavior,以看到你的源代码。对于我们刚刚建立的测试,生成的测试用例代码如下:
package test;
import junit.framework.*;
import example.TestableExample;
import com.ibm.rational.test.ct.execution.runtime.ComponentTest;
public class TestableExampleTest extends TestCase {
public void test_getDataValue() {
TestableExample objTestableExample = new TestableExample();
String data = "";
String retValue = "";
retValue = objTestableExample.getDataValue(data);
}
}
|
一个新项将会出现在你组件测试项目的Test Suite 文件夹下的Test Navigator中。该项将以类"Test"命名。扩展该项,你将会发现一个代表getDataValue方法的节点。如果你已从更大的类中选择了其它方法,就可能在测试中看到像是节点的方法。
运行一个组件测试
让我们继续,运行建立的测试。
- 右击测试组(TestableExampleTest)并选择 Run > Component Test。
你将在Test窗口的右下角看到背景执行的状态。当测试完成时,你可以在组件测试项目的Run文件中看到一个新的节点。该节点以测试中的组件命名,而且包括"Configuration"、日期及名字中的时间标记,如图2所示。
图 2. Test窗口,显示Run文件夹中的新节点
如果关注测试执行,那么,你会发现这个测试成功地执行完毕。你也应该看到Test Data Comparator的新视图。这个视图显示了真实的实施测试和每个测试的结果。既然是这样,你应该看到每个过去的测试。这里显示了两个测试:第一个展示了类构造器的成功执行,第二个是真实方法调用的执行。我们可以从这个测试中得到的唯一真实的信息是我们的代码均没有出现任何意外。我们仍然没有真正检查我们的代码。
用测试数据扩展框架
我们能够执行方法,这是非常不错的,但是,因为我们不能够验证输出,所以其实并不能真正证明什么。我们建立的简单的可测试的类将返回传递的任何字符串,所以让我们评估一下这个结果。如果我们对未完善的JUnit类编码,那么写出来的代码就有可能如下所示:
assertEquals("a", objTestableExample.getDataValue("a"));
assertEquals("a", objTestableExample.getDataValue("b"));
|
该示例中,存在几个很明显的问题。第一,数据值是硬编码。第二,每个数据验证点需要代码修饰。这是Test Data Table (TDT) -- IBM Rational Application Developer for WebSphere Software的一个特性 -- 能够为我们节约很多时间与精力。
- 选择文件TestableExampleTest.java。如果它不可见,在Test Navigator pane中右击测试项,并选择 Open Behavior。
- 将鼠标定位在getDataValue方法上,则TDT将显示在TDT pane中。
每个测试方法都有其惟一的TDT。如果你在测试用例中用到了多方法,可以发现在任何方法上单击均会显示与当前代码内容相匹配的TDT。在TDT中,你可以看到嵌套数据的两个测试动作块。每个都代表一个可测试的事件。你每次保存这些源文件时,便调用测试类的解析。TDT自动识别变量和任务并建立匹配的动作。
- 将这些代码添加进测试行为类:
String x = "";
String y = "";
x = String.valueOf(y);
|
- 单击 Save,你将注意到在TDT中出现了新的语句块
assignment x = String.valueOf(y)。
如你所见,TDT表示了测试用例的良好粒度,允许你以不同的方法操作数据。当探索TDT的复杂性时,我们将代码留在这个更新的状态。
- 在方法块中单击,以便TDT上下文的排列。
如果你看到TDT加亮显示,这说明你处在流状态,你已作了一些改动,但是却没有保存。只要你理解它发生的原因,这点便是一个让人满意的警告提示。
在TDT面板的右侧,你将看到两列的Test Data组:"In" 和 "Expected"。在此处,我们定义输入参数及期望的结果。
- 在 "In" 列右击
getDataValue 验证的数据行。
- 选择 Define Simple,编辑列。如果你已处在该列的编辑模式,则会看到一个弹出式菜单。
Test Data Table
- 在列中输入一个字符串值"a",并确保包括引号。
- 移动到"Expected"列的"retValue"行,然后也输入一个字符串值"a"。
- 继续进行到
String.valueOf(y) 验证,并在 "In" 列的 "y" 行和 "Expected" 列的 "x" 行中输入字符串值 "1"。
修改TDT时,你会发现小磁盘图标是可用的。保存输入的数据,单击该图标或者按下Ctrl-S即可。请确保每次更改数据之后都要执行这一步。
运行修改后的组件测试
现在,让我们再次执行测试,这次使用装载的数据。
- 右击测试组(TestableExampleTest)并选择 Run > Component Test。
测试窗口
你将再次在Test窗口的右下方看到背景执行的状态。当该测试完成时,你会在组件测试项目的Run文件夹中看到一个新的节点。结果的Test Data Comparator (TDC)面板如图3所示。
图 3. 在Test Data Comparator使用数据的执行结果
你可能注意到,Test Navigator仍然像Test Data > Individual Test #0一样,在Run文件夹中表示测试的结果。但是也注意到Test Data Comparator现在为我们的测试展示了期望的、真实的值。这次,我们真正验证了我们的组件确实执行了我们所期望的事情。
测试数据的其它用途
让我们尝试一些更加复杂的数据场景,并看看会有什么影响。
- 在源代码中单击,显示测试用例的TDT。
- 在每个数据值列右击,并选择 Define Set。(注:请确保TDT面板足够大地显示整个设置定义弹出式窗口。这个最初会给我们带来一些问题)
- 将值"a"更改成"a" 和 "b",输入每个值后单击 Add 按钮。
- 继续进行,更新所有列,将值"1"更改成"1"和"2"。Test Data Table将被这些新值所填充。
Test Data Table
- 再次运行测试,完成时扩展运行结果。你现在可以看到单个测试的节点(从#0到#3)。
Test Navigator 面板
你也许会问到,为什么我只定义两组,但是却有四个测试在运行?你的感觉真敏锐!因为TDT 推断出了四个不同场景--a/1, a/2, b/1, and b/2。这些数据的结合导致一个独立的测试执行。单击每个测试以查看每个场景的具体结果。
测试数据的更多用途
现在,让我们真正活跃起来!你可能已经注意到测试结果上方的Test Data节点。该节点直接来源于Test Data Table的组。我们能够改变这个组,并添加更多组,以建立更加复杂的测试。
- 回到TDT(在源代码中单击)。
- 右击包括标题"Test Data."的标题栏。
- 选择 Rename Data Set,并将名字改为"Pattern 1." 你会发现标题的改变。
- 再次右击并选择 Add Data Set。
- 选择标题"New Test Data",并改名为"Pattern 2."
- 在"Pattern 2"列中,将
getDataValue 和 String.valueOf(y) 的"In" 和 "Expected" 值分别改为"c"和"3"。
Test Data Table
- 再次运行测试。
执行完成之后,你会在Test Navigator面板中发现被数据集细分的结果(如图4所示)。如果你的命名比Pattern 1 and Pattern 2更有意义,这点是非常有用的。
图4. Test Navigator面板中由数据集细分的结果
导出测试结果
测试执行结果能够以HTML报告的形式导出。这允许结果在整个测试团队中都可用,解析结果,发送e-mail,或者用它们做一些其它有用的事情。
- 选择 File > Export > Component test execution results,然后单击 Next,显示导出结果的向导。
The Export Results 向导
- 选择你想要包括在报告中的测试运行。
- 为报告选择目标文件夹和选项。
- 单击 Finish.
报告在目标文件夹中以HTML的格式生成。如果你选择"Only one HTML file"选项,结果报告将是目标文件夹中支持图象图标的一个HTML文件。如果不选择该项,则默认构建一个报告文件,该文件概述了选定的运行,并为每个选定的实执行建立单个HTML文件。报告的详细内容以HTML格式重复Test Data Comparator面板。
导入已有的JUnit测试用例
如之前所提到的,组件测试功能是JUnit框架的扩展。同样地,你能够运行组件测试设施中的任意既有JUnit测试用例,如果你将它们导入到一个组件测试项目中,并且如果JUnit源代码在你的工作区中(足够轻松处理的一个限制)。请注意本文的撰写过程中,由于产品之间的差异, 所以在IRAD的不同版本之间变换时我们遇到了一些类路径的问题。如果你在导入时遇到同样的问题,请双击包括在项目中的类库。
让我们在最初的Java项目中建立一个包,命名为test,构建JUnit类,命名为ImportedTestCase,然后用以下源代码代替已有代码:
package test;
import junit.framework.*;
import java.lang.String;
public class ImportedTestCase extends TestCase {
public void testSomething() {
String value1 = "a";
String value2 = "a";
String value3 = "b";
String value4 = "b";
assertEquals(value1, value2);
assertEquals(value3, value4);
}
}
|
将该测试引入组件测试项目中。Test Navigator并不展示Java源文件,所以你会需要适当地改变视图。
- 选择 File > Import。
- 选择 "JUnit test into component test project" 作为导入源。
- 在 the Select a Test Project window 中单击 Next。
- 从Java项目中选择测试用例并单击 Finish.
Import JUnit Test 对话框
如果你在导入前关注JUnit Java的源代码,你可能仍然会在导入后再看同样的文件。在我们测试的过程中,视图并没有改变,并且没有打开或者关闭任何文件。这是一点小小的误导,因为源代码已被变换并且现在处于组件测试项目中。
请确保关闭Java源文件,然后在新的测试用例中右击,然后选择 Open Behavior 访问新的源文件。导入之后,你需要验证Test Data Table内容。在我们的测试中,源代码得到了适当的变换,但是有些数据值不能够被完全填充。主要的区别在于 assertEquals 方法已被 ComponentTest.check 方法所代替,如图5所示。
图5. 导入后的JUnit测试用例
你现在可以像我们前面所做一样,随意地更新任何数据值或者改变测试用例。
警告
当我们使用IRAD感到困惑的时候,Allen注意到测试数据均在二进制文件中用Java源文件来进行管理。我们发现了两个潜在的问题:
- 导入或导出数据不存在简单的方法。代码仍然在JUnit框架中,数据表和代码没有关联并且完全被IRAD管理。
- 如果你正在一个团队中工作,你将需要得到数据文件的排它锁(.nextsuitebehavior),因为你不可能在版本控制系统中合成文件。
我们都不觉得以上问题会阻止我们使用这个工具,并且认为这些问题是值得指出来的。
测试人员和开发人员共同使用IRAD
从测试的角度来看,Mike不知道开发人员实际上拥有建立复杂组件-测试场景的能力,选择输出数据的设置和范围,或者已准备好访问前面所提到的静态度量。从开发的角度而言,Allen当我们开始说到数据选择技术和测试人员如何使用类似于边界-值分析、领域分割、等价类分割的概念来为最佳的测试用例涵盖选择数据设置和范围。
我们建议开发组件测试的时候,测试人员和开发人员能够共同工作。这里所说的“共同工作”并不一定意味着在开发测试时并行工作(尽管那样做也没有任何问题)。我们预想一个场景,开发人员他们自己的开发组件测试,然后和测试人员一起工作,来确定要在测试中使用的测试数据。数据可以由测试人员直接输入,这样,扩展了组件测试,或者测试人员可以就数据选择提出一些建议,由开发人员输入数据。
这样工作可以带来以下优点:
- 不断增长的技能的鉴别和理解
- 交互训练和知识传播
- 更好且更有意义的组件测试
- 项目生命周期中功能测试下行的组件测试数据可能的复用
- 基于涵盖率统计和其它静态度量的所有测试的更好配合
- 更好的软件
参考资料
作者简介  | |  | Mike Kelly 现在是Fusion Alliance在Indianapolis的顾问。他拥有管理软件自动化测试团队的经验,并且从1999年开始使用Rational工具。他主要的领域是软件开发生命周期、软件测试自动化和项目管理。你可以通过email Mike@MichaelDKelly.com与Mike取得联系。 |
 | |  | Allen Stoker 是Liberty Mutual在Regional Agency Markets (RAM) division的技术专家。自1983年以来,他曾在多种平台上实施系统,包括IBM mainframe, Windows,和J2EE,并且现在的主要研究领域是J2EE环境中的质量驱动开发。他最近负责推动RAM机构单元测试实践的实施。你可以通过email Allen.Stoker@PortalWizard.com与Allen取得联系。 |
对本文的评价
|