一种减少 Rational Functional Tester 测试脚本开发迭代次数的方法
 

2010-02-26 作者:周善玉,李冬瑞 来源:IBM

 
本文内容包括:
本文采用循序渐进的方式介绍了一种减少 Rational Functional Tester(RFT)测试脚本开发迭代次数的方法。该方法综合调试、异常处理、流程控制等技术,以调试的方式进行脚本开发,能够实现快速开发脚本的目的,特别适合于中大型测试脚本的开发。

这篇文章介绍了一种减少 RFT 测试脚本开发迭代次数的方法。

通过学习这篇文章,您可以了解:

  • IBM Rational Functional Tester 脚本的传统开发方式
  • IBM Rational Functional Tester 测试脚本的结构特点
  • IBM Rational Functional Tester 的断点调试技术
  • 一种新的脚本开发模式

手工开发 Rational Functional Tester(RFT)测试脚本的过程和开发普通的程序类似,需要经过“编写、执行、发生错误、退出执行、查找并修改错误、再次执行脚本……开发成功”的过程。其中“执行、发生错误、退出执行、查找并修改错误”几个环节可能需要多次的循环迭代。对于步骤繁多的大型脚本,上述迭代次数的大幅增加将会产生巨大的时间成本。本文提出一种实现快速开发脚本的方法,该方法综合调试、异常处理、流程控制等技术,以调试的方式实现脚本的开发。该方法把编写和调试脚本的过程合一,可以一边开发脚本,一边执行脚本;当脚本执行出现错误时,能够迅速定位错误,并且在修改错误之后无需从头执行脚本。这种方法能够有效地缓解传统开发方法存在的迭代问题,特别适合于中大型测试脚本的开发。

本文主要面向使用 RFT 且以手工方式开发 Java 型测试脚本的工程技术人员。我们假设你已具备如下的知识背景:

  • 对 RFT 和 RFT API 已有基本的掌握,并具有手工开发测试脚本的经历。
  • 基本掌握 RFT 的调试技术。
  • 了解 Java 的异常处理机制。

传统开发模式中的迭代问题

为提高代码的重用性,测试人员一般不直接录制功能性自动化测试脚本,而是采用手工方式开发测试脚本,传统的开发流程如图1所示:

图 1. 传统的脚本开发流程
图 1. 传统的脚本开发流程

一般来说,为开发一个测试脚本,在编写脚本之后,需要反复迭代上图所示流程中的几个环节:执行脚本、抛出错误、退出执行、查找错误、修改错误、再次从头执行脚本。为了快速开发脚本和复用代码,测试人员虽然可以使用特定的自动化测试框架来开发脚本,但脚本的开发过程仍然遵循上述的步骤。

图 2 展示的是一个自动化测试脚本的片段,它开发于特定的测试框架。在编写该脚本之后,需要执行脚本以验证脚本的正确性。

图 2. 测试脚本片段
图 2. 测试脚本片段

如果脚本片断中步骤 2 的代码存在错误(例如,找不到相应的操作对象),RFT 通常将会异常退出,结束脚本的执行(另外的可能是弹出窗口,让测试人员更新对象的识别方法),我们需要查找并修改代码中的错误,然后重新执行脚本。如果脚本再次发生错误,我们将继续重复“退出执行、定位错误、修改错误、再次执行脚本”的过程。

为了避免从头执行脚本引起的时间消耗,通常测试人员采用注释的方式对脚本进行调试,例如,注释掉步骤 2 之前的代码(见图 2),然后重新执行脚本,RFT 将从步骤 2 开始执行相关的测试操作。虽然这种方法可以节省一定的时间,但是反复地对代码进行注释容易使脚本逻辑结构变得混乱,有时反而不利于调试。

图 3. 以注释方法进行调试
图 3. 以注释方法进行调试

注:这里只能选择性地注释掉其中的一些操作语句,像变量定义这样的语句不能被注释掉。

如果测试脚本隐含着多处错误,上述的开发方式就必须多次迭代“退出执行、查找错误、修改错误、再次执行脚本”的过程。

对于小型的测试脚本,由于代码规模小、错误数量有限,迭代引起的时间成本是比较低的。然而对于大型的测试脚本,随着代码规模的扩大,出错的可能性和频率也会随之增加,大量的迭代将会导致时间成本的大幅增长。因此,如何尽量减少迭代的次数,对于开发大型脚本具有重要的意义。

下面我们开始介绍一种可以减少迭代次数的开发模式。

减少迭代的开发模式

这里先简单介绍一下我们的设想。

基本设想

为了缓解传统开发方式存在的迭代问题,实现快速地开发脚本,我们设想:

  • 把脚本的编写与执行过程合一,一边编写脚本,一边执行脚本,那么脚本编写的结束就意味着脚本执行的成功。
  • 在定位并修改脚本错误之后,允许从出错代码开始恢复执行脚本,就可以避免从头运行脚本带来的时间消耗。
  • 当脚本执行发生错误时,能够立刻暂停执行脚本(而不退出)并保持出错现场,这对于我们迅速定位和修改错误将是十分有帮助的。

理论技术依据

我们能否用现有的技术来实现上述的设想呢?答案是肯定的。为了实现这些设想,我们可以借助的理论依据和技术有:

  • 测试脚本的结构特点:自动化功能测试脚本具有线性的特点,脚本代码大多呈现为顺序结构(见图 2 的测试脚本)。脚本中很少出现选择或者循环结构的代码,即使脚本中出现非顺序结构的代码,其覆盖范围也是有限的。这种结构特点为分割脚本代码提供了可能。
  • 测试步骤的可重复性:测试步骤主要描述对产品界面进行什么操作,操作引起的产品状态变化可以通过人工的方式进行回滚,因此,我们可以反复执行某些测试步骤,只需要在每次重复执行之前消除测试操作所造成的影响(例如数据、产品界面的变化)。
  • 断点技术:在软件调试技术中,断点技术是最为常用的技术之一,通过在脚本中设置断点,我们能够在需要的地方暂停脚本的执行,以便检查脚本以及产品的状态(例如对象以及变量的内容,产品界面的变化等)。我们通过在调试状态下动态地设置断点,从而可以控制脚本的执行进度。
  • 异常处理机制:Java 语言所提供的异常 (Exception) 机制用于捕获和处理程序发生的异常。我们同样可以使用异常机制捕获脚本中出现的错误和异常。通过在异常处理语句中设置断点,可以在脚本出错时,立刻捕获脚本错误并暂停执行脚本,同时也能够获得第一出错现场:产品界面,变量和对象的内容。
  • 热交换错误修正功能:RFT 构建于 Eclispe 平台之上,而 Eclipse 提供了热交换错误修正(Hotswap Bug Fixing)的功能。它允许在调试会话过程中实时地更改源代码 , 而不必退出程序的执行状态,关于该功能的介绍参见参考资源 1。借助热交换错误修正功能,我们可以达到在执行状态下开发脚本的目的。

新的脚本结构

在介绍具体的开发流程之前,我们首先介绍一种新的脚本结构。该结构不同于传统的脚本结构:它使用选择结构和循环结构作为实现分割脚本和控制测试步骤执行顺序的主要手段。图 4 中的代码描述了这种新的脚本结构。

图 4. 新的脚本结构代码
图 4. 新的脚本结构代码

下面我们对结构代码的各组成部分逐一进行介绍:

  • 变量定义

RFT 测试脚本体现为一个类,测试步骤一般被放置于某一类方法中。对于脚本中需要共享的数据,例如文件名、组件名、需要进行对比验证的数据等,我们使用类变量或者方法内部变量的形式进行表示。

  • switch-case 结构

switch-case 结构用于划分测试脚本,每个 case 分支存放一个测试步骤的代码。case 语句的条件值对应测试用例中的测试步骤号。

  • 循环结构

for 循环语句用于控制脚本的执行过程,循环变量(见图 4 中的变量 step)指明每次循环需要执行的 case 分支(测试步骤),在调试状态下,我们可以通过修改该变量的值指定将要执行的测试步骤。

  • 异常处理语句

我们使用 try-catch 结构包含整个循环结构,也就是全部的测试步骤。然后在 catch 语句处设置断点,并在其中添加如下语句:

System.out.println("Step " + step + " throw: "+ e.getMessage())。

通过在 catch 语句设置断点,可以在脚本抛出异常(Exception)或错误(Error)时,捕获脚本发生的问题并立刻暂停执行脚本。在暂停执行脚本以后,我们可以通过单步执行异常处理语句来定位出错的 case 分支(测试步骤),同时也可以使用 RFT 相关工具(例如 Test Object Inspector)以及相关的调试工具来辅助定位错误;我们还可以借助热交换功能修改出错的代码,或者修改变量 step 的值以指定 RFT 将要执行的 case 分支。鉴于测试脚本不同于普通的应用程序,对于脚本抛出的错误(Error)不一定必须退出脚本的执行。因此我们在 catch 部分声明捕获 Exception 类和 Error 类共同实现的接口:Thowable.。

开发流程

在介绍完脚本结构之后,下面我们以流程图的形式给出脚本的具体开发流程(见图 5),并对其中的几个环节予以说明。

图 5. 开发脚本的流程
图 5. 开发脚本的流程

下面我们对流程图中的几个主要操作环节的细节进行说明:

  • 编写脚本结构代码

建立图 4 所示的脚本结构代码。确保每一个 case 语句对于一个 break 语句 , 然后把测试步骤的描述以注释的形式写入对应的 case 分支。由于脚本结构比较固定,因此也可以事先定义一个脚本结构代码模板,以便每次开发新脚本时能够复用已有的结构代码,从而减轻开发的工作量。

  • 设置断点

在编写测试步骤代码之前,首先把以下两个语句设为断点:

  • case 0 分支中的 break 语句(见图 4 断点 1);
  • catch (Throwable e){}(见图 4 断点 2);

为了控制每次脚本执行的进度,我们可以把每个 break 语句都设置为断点,以便写一段代码,就执行该段代码,从而达到一边编写脚本,一边执行脚本的目的。

  • 用调试模式执行脚本

通过点击图 6 所示的按钮,用调试模式执行脚本。然后根据暂停执行时的断点类型进行不同的操作:

    • 如果脚本暂停于 break 语句,说明当前的(第 step 步)测试步骤代码执行成功,可以开始编写下一个(第 step+1 步)测试步骤的代码。
    • 如果脚本暂停于 catch 语句,说明当前的测试步骤代码有错误,需要对代码进行修改。
    • 如果脚本没有暂停于任何断点,说明已完成测试脚本的开发工作。
图 6. 用调试模式执行脚本
图 6. 用调试模式执行脚本
  • 保存代码,并恢复脚本执行

在调试状态下,修改代码并执行保存操作会导致执行点转移到当前方法的开始语句。点击图 7 所示的按钮可以恢复执行暂停的脚本,那么 RFT 将会执行 step 指定的 case 分支。

图 7. 恢复脚本的执行
图 7. 恢复脚本的执行

脚本开发实例

下面我们以图 2 的脚本为例,介绍具体的开发过程:

  1. 首先定义脚本中变量,本例中定义了两种形式的变量:类成员变量和方法内部变量,具体定义的内容见图 8。
图 8. 开始编写测试脚本
图 8. 开始编写测试脚本
  1. 编写脚本结构代码之后,把测试步骤描述以注释的形式写入对应 case 分支,并设置断点。此时的代码内容如图 8 所示。
  2. 以调试形式启动执行脚本之后,执行将会暂停于第一个断点,见图 8 中的第 51 行。
  3. 在脚本暂停执行之后,编写测试步骤 1 的代码于 case 1 分支,并设置 case 1 对应的 break 语句为断点。
  4. 修改变量 step 的定义:int step=0; 修改为 int step=1;
  5. 存盘并恢复执行脚本;当进行存盘操作以后,执行点将转移到当前方法的开始语句 , 即:int step =1;(见图 9),在恢复脚本执行以后,则 RFT 将跳过 case 0 中的测试代码,直接执行 case 1 分支(step 指定)中的代码。由于测试步骤 1 的成功执行,执行将暂停于 case 1 对应的 break 语句。
图 9. 执行点转移
图 9. 执行点转移
  1. 接下来,编写测试步骤 2 的代码于 case 2 分支,并修改变量定义: int step=1int step=2 ,将 case 2 中的 break 语句设置为断点,然后存盘并恢复脚本执行,则 RFT 将跳过 case 0 和 1 分支,直接执行 case 2 分支的脚本。
  2. 由于 case 2 分支代码有误(假设这里测试人员把 d1 误写为 d2,而实际上 d2 对象并不存在),执行过程中抛出的错误将被捕获,脚本将被暂停于 catch 语句(见图 10)。此时的产品界面被完整的保留(见图 11)。
图 10. 捕获脚本错误
图 10. 捕获脚本错误

图 11. 脚本出错时的产品界面
图 11. 脚本出错时的产品界面
  1. 在暂停状态下,可以使用 RFT 提供的相关工具(例如 Test Object Inspector)来帮助分析和定位错误。在本案中,通过直接观察对应产品界面和 case 2 分支中的代码就能得出结论:d1 被误写为 d2。
  2. 在修复错误之后,可能需要对测试操作进行手工回滚,把产品状态恢复至当前测试步骤执行之前,以便接下来重复执行出错的测试步骤。在本案例中,由于 case 2 分支的第一行代码就发生错误,还没有对产品产生影响,因此不需要进行任何回滚。
  3. 保存脚本代码并恢复脚本执行,RFT 将会执行 case 2 分支的代码。
  4. 如果第二个测试步骤执行成功,执行将暂停于 case 2 分支的 break 语句 , 接下来可以开始编写测试步骤 3 的代码。否则我们需要重复 8~11 步的操作。
  5. 对于剩余的每个测试步骤重复类似测试步骤 1 和 2 的开发过程,直到整个脚本开发完毕。

验证脚本

完成脚本开发以后,我们再次执行脚本来确认脚本的正确性:首先去除所有的断点(仅保留 catch 语句处的断点),修改循环变量 step 的值为 0,指明 RFT 从头到尾地顺序执行所有测试步骤。然后以调试的形式执行脚本。

由于每个测试步骤的代码都是以调试方式开发的,出错可能性已非常小,即使出现错误,我们也能够通过 catch 语句迅速地捕获、定位并予以更正。

适用范围及注意事项

  • 适用于开发中大型脚本

在实际工作中,我们曾经开发过一个执行时间长达 40 分钟的测试脚本,如果使用传统方法进行开发,一般至少需要 2 天的时间。借助于本文提出的开发方法,我们在不到 1 天的时间内就完成了开发工作,从而节约了大量的时间。

  • 脚本无法构建于 testmain() 方法内部

RFT 测试脚本文件的入口类方法是:

public void testMain(Object[] args)

RFT 执行脚本时会自动调用该方法。由于 RFT 不支持对该方法内的代码进行热交换错误修正,因此不能在该方法内开发脚本代码。我们可以选择在其他类方法中开发测试脚本,然后通过 testMain() 方法调用该方法。

  • 修改类成员声明限制

调试状态下,对该类成员声明的编辑都会导致 RFT 出现异常,所以不要在调试状态下编辑类成员声明,总之,类成员变量只适合于存放固定不变的共享数据,而对于容易发生改变的数据应该使用方法内部变量进行存放。

总结

本文提出的脚本开发方法借助了 Rational Functional Tester(RFT)的调试技术和特点,把脚本的编写过程与执行过程合成为一个过程;该方法通过综合运用多种技术,在多数情况下可以保证脚本开发的一次性成功,从而解决了传统方法存在的迭代问题,实现脚本的快速开发。另外,采用该方法开发的代码符合软件可调性的基本原则,例如:最短距离原则、最小范围原则、立刻终止原则、可控制原则、可重复原则等(可参见参资源 2),因此,脚本具有良好的可调试性,易于其他测试人员进行调试和维护。

我们在实践中主要应用该方法开发测试网页产品的测试脚本,是否适用于其他类型的产品,还有待于实践的检验。

参考资料

学习 获得产品和技术 讨论
火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。

资源网站: UML软件工程组织