转自:http://www.ibm.com/developerworks/cn/rational/r-cn-guiautotesting4/
定义测试控件库
本系列前几篇文章对 IBM 框架做了介绍,相信大家已经有了初步的认识。在三层架构的应用对象层中,又包含两部分内容:测试控件库和用户界面定义。我们可以使用测试控件库中的类来定义被测程序的对象。这样做的好处是增加测试代码的重用性,将 Rational Functional Tester(以下简称 RFT)的基本接口都封装到底层的少数几个类里面,再通过继承的方式重用在不同的控件里。测试控件库中除了包含一些像按钮、标签之类的标准控件,也可以包含一些产品中实现的自定义控件,比如在 Lotus Notes 中,我们的工具栏、富文本框都是这种自定义控件。下面,让我们以 Lotus Notes 为例,看一看如何来定义测试控件库。
RFT 的基本原理
要了解如何定义控件对象,特别是自定义控件的对象,首先我们需要了解 RFT 的内部实现原理。简单说来,RFT 与被测程序间的通信可以归纳为以下四点:
- RFT 提供了一系列继承自 TestObject 的对象供脚本调用。
- 测试对象 (Test object) 在被测程序中有对应的代理对象 (Proxy objects),并且通过共享内存与之通讯。
- 代理对象包含被测程序界面控件的引用,并能够调用界面控件的方法。
- 测试对象的方法最终由代理对象实现。
在图 1 中,我们可以比较明显的看出这种交互关系。
图 1. RFT 测试对象模型
RFT 提供的测试对象 API
测试对象(TestObject)提供了一组基本方法用来和被测程序中的代理对象(ProxyObject)进行通信,包括:
find
:遍历控件所属层次结构中包含的所有子控件,返回符合匹配条件的测试对象getProperty
:获取控件的属性invoke
:调用控件的方法getMethods
:得到控件可以调用的方法的列表,配合 invoke 使用
通过使用这 4 个方法,RFT 能够支持所有的 Java 控件,包括标准控件和自定义控件。以按钮为例,让我们看一看如何使用测试对象来实现点击按钮的操作。
首先,我们需要得到控件对应的测试对象。如例 1 所示 , 我们可以直接调用 RootTestObject.find
方法,遍历当前运行的所有应用程序中的控件,返回符合匹配条件(类名是 org.eclipse.swt.widgets.Button
)的测试对象。然后再做进一步筛选,得到名称是“OK”的按钮。
例 1. 获取 OK 按钮的测试对象
TestObject [] tos = RationalTestScript.getRootTestObject().find( SubitemFactory.atDescendant("class", "org.eclipse.swt.widgets.Button")); for(TestObject to:tos){ if("OK".equals(to.getProperty("text")){ return to; } }
显然,在实际使用时,我们还需要对例 1 的代码进一步完善,考虑更多的细节,比如:
- 可以先找到控件位于的窗体,再从窗体对应的测试对象查找。这样能够提高定位准确性和缩短查找时间
- 匹配文字之前,检查:
- 控件是否有效——调用
to.invoke("isDisposed")
- 控件是否可见——调用
to.invoke("isVisible")
- 控件是否有效——调用
- 处理异常,包括
PropertyAccessException
和WrappedException
。由于测试程序运行中,某些控件处于无法访问的状态,我们需要对这两种异常进行处理。
在得到按钮后,我们接下来需要实现点击操作,如例 2 所示。
例 2. 点击按钮
Rectangle r1 = toBtn.getProperty("bounds"); // 按钮的 bounds 属性 Rectangle r2 = toDlg.getProperty("bounds"); // 按钮所在窗体的 bounds 属性 // 计算按钮位置 int x = (int)( r1.getWidth()/2 + r2.x); int y = (int)( r1.getHeight()/2 + r2.y); RationalTestScript.getScreen().click(new Point(x,y)); // 点击按钮
熟悉 RFT 的朋友可能会发现,例 2 实际上等效于 new GuiTestObject(to).click()
,但是 GuiTestObject
只支持标准控件,对于自定义控件,RFT 往往不能将 TestObject
构造成 GuiTestObject
。这种情况下,例 2 介绍的方法就能派上用场了。
总结一下,通常说来,操作控件的基本思想就是两步:首先获取控件位置,然后根据需要控制鼠标和键盘,完成对控件的操作 (click, inputKeys)。
查找用户自定义控件
了解这些原理后,如何测试自定义控件的思路就很清晰了。用户自定义控件的一个特点,就是不能被 RFT 识别。Notes 工具栏按钮就是一个例子,用 RFT 识别时,我们只能看到整个工具栏,而没有办法抓取工具栏上的每一个按钮。这个时候,我们需要使用 invoke
调用控件自己的方法去获得这些按钮对应的测试对象。如例 3 所示,toParent
是工具栏的测试对象,由于它能够被 RFT 识别,我们很容易使用例 1 的方式得到它对应的测试对象。通过调用 getMethods
, 我们能够看到 toParent
有个 getItems
函数,可以返回按钮控件数组。虽然 getItems
是被测程序内部的方法,返回的是控件的对象,但是 RFT 在 invoke
调用时,能够自动为控件对象创建代理对象,并返回测试对象供脚本端使用。
例 3. 定位工具栏按钮
TestObject [] tos = (TestObject[]) toParent.invoke("getItems"); for(TestObject to:tos){ if("Copy".equals(to.getProperty("text")){ return to; } }
限于篇幅,对于如何使用 RFT 操作测试控件我们在这里只能介绍基本原理,关于 RFT API 的详细使用方法大家可以参照附录中的《使用 Rational Functional Tester 的 getProperty
和 invoke
方法测试定制的 Java 控件》一文。
使用面向对象的思想封装测试对象
定义完程序中的这些控件和操作方法,接下来我们需要将这些控件和操作封装到有层次关系的对象中,组成测试控件库。仍以简化的按钮为例,我们可以简要了解一下控件库的结构。如图 2 所示,JControl 是整个控件库的基类,它实现了获取 TestObject、点击、设置焦点等通用操作;从 JContrl 直接和间接派生了 3 个类,其中 JButton 和 JCheckBox 是标准控件,JToolbarButton 是自定义控件。对于标准控件来说,只需要扩展基类没有的方法,比如 JCheckBox 需要通过 TestObject 实现自身的 check/unCheck/isChecke
d 方法;而自定义控件往往需要覆盖基类的某些方法。比如 JToolbarButton 由于不能被 RFT find
方法找到,必须 invoke("getItems")
得到,它需要覆盖基类的 findTestObject
来定制这部分功能。
派生类的另外一个特定是简化构造函数,因为派生类隐含着类名,例如 JButton 表示它的类名是 Button, 构造时不必要求用户输入类名。通过合理实现 JControl.findTestObject
方法,可以支持更多的控件属性,包括 index
和 id
。这些属性跟 text
一样,可以用到构造函数里面。除了这三个属性,正则表达式也可以用在构造函数中。
图 2. 测试控件 UML 图
自动生成应用对象层的用户界面定义
当我们拥有了完整的测试控件库之后,为每一个用户界面定义一个对象实际上成为了一个机械的工作,有固定的模式和规则可循。所以,我们完全可以通过编写工具来简化用户界面定义工作。
图 3. 用户界面示例
以图 3 所示的对话框为例,我们在定义用户界面类的时候,需要为每一个控件加入一个成员变量,并加入一个 get
方法,使得我们可以直接获取并操纵用户界面上的每一个测试控件。我们可以简单的把这一过程对应到程序逻辑,包括以下几步:
- 找到待生成定义的对话框。(可通过鼠标选择,或假定为当前活动的对话框即为待测对话框)
- 通过
TestObject.getChildren()
函数获得并遍历对话框上的所有控件:- 通过
TestObject.getProperty(".class")
获得控件的实际类型 - 结合控件类型和控件的其他属性(如“标题”)生成测试对象实例。
- 生成用于获取该测试对象实例的
get
方法。
- 通过
在实际实践中,我们可以选择把这段逻辑直接编写为一段测试脚本,而把生成的类定义信息输出到日志中。我们只需要在合适的位置定义用户界面对象,再把生成的信息粘贴进去就可以了。在生成类信息的同时,我们也可以考虑同步生成单元测试的代码,用来对新生成的用户界面类进行初步的单元测试。
自动录制测试脚本
还记得我们在第一篇文章中介绍的 RFT 录制和回放功能吗?自动记录操作细节能够使得创建测试脚本变得更加便捷,然而,RFT 自身的录制功能还不能和我们的 IBM 框架有机结合,支持我们定义的测试控件库和用户界面定义。不过,当我们把框架的基础设施建立完毕后,可以考虑着手创建适合项目的简易的录制工具。
录制工具并不需要是独立的应用程序,和自动生成用户界面定义的工具一样,我们可以以 RFT 自身提供的 API 和解析机制为基础,创建一个 RFT 测试脚本,将这个脚本作为录制工具运行的入口。这样,脚本里面的 Java 代码便能使用所有 RFT 的接口,并且能够利用反射的方式,调用应用对象的类。编写这一工具需要较大的工作量,下面,就让我们看一看基本的思路。
录制功能需要的 API
要实现一个录制工具,我们首先需要解决三个基本问题:捕捉键盘鼠标事件、获得当前窗体的标题和属性,以及获取鼠标选中对象的属性。这三件事情都可以使用 RFT 提供的 API 来实现。下面分别给出实例代码说明如何用 RFT 的 API 来实现这三点。
- 使用 LowLevelRecorder 类捕捉键盘鼠标事件
RFT 自身的录制功能依靠 com.rational.test.ft.sys.graphical.LowLevelRecorder
截获操作系统的消息,并用来记录用户的操作。这个类同样可以用在我们自己写的应用程序中。例 4 展示了如何捕获底层鼠标事件。
例 4. 使用 LowLevelRecorder 类捕捉鼠标键盘事件
final LowLevelRecorder recorder = new LowLevelRecorder(); recorder.addEventListener(new EventListener(){ public void LLMouseEventOccurred(final LLMouseEvent event) { if(event.modifiers == 0) return; // ingore mouse move event if(event.kind == 1){ System.out.println("clicked mouse left key at: " + event.point); } } public void HLKeyEventOccurred(HLKeyEvent arg0) {} public void LLKeyEventOccurred(LLKeyEvent arg0) {} public void RecordingWasAborted() {} }); recorder.start();
- 获得当前窗体的标题和属性
因为 IBM 框架定义的应用对象通常跟一个窗体对应,录制工具首先需要知道点击控件位于什么窗体上面。getActiveWindow
是 RFT 提供的获取窗体标题和属性的一个有效方法。如例 5 所示,getActiveWindow
返回一个基于 IWindow
接口的对象。这个对象其实基于 RFT 的 TestWindow 类,与 TestObject 不同,它保存着窗体的句柄(handle),通过调用 IWindow
的 API,我们可以获取窗体的一些属性,包括窗体类型和标题等。
例 5. 获得当前窗体的标题
IWindow active = RationalTestScript.getScreen().getActiveWindow(); String sCaption = active.getText();
- 获取鼠标选中对象的属性
当录制工具捕捉到鼠标点击事件时,需要立刻记录对应位置的控件属性。RFT 提供了 objectAtPoint
的方法来获得。例 6 演示了如何利用objectAtPoint
来得到指定点的控件属性。properties
是一个 HashMap,里面包含了 RFT 找到的所有属性信息,比如对象的 class
属性。后文会提到如何应用这些属性是生成录制代码。
例 6. 获得指定点的控件属性
RootTestObject root = RationalTestScript.getRootTestObject(); TestObject to = root.objectAtPoint(point); //point 是鼠标点击的位置 properties = to.getProperties();
实现适合 IBM 框架的录制器
在解决基础技术问题之后,我们需要考虑如何生成适合项目层次结构的测试代码。下面以图 3 中的 Lotus Notes 拼写检查对话框为例,分三个层次介绍如何录制出符合 Lotus Notes 测试项目的代码。录制工具的目标就是当用户点击 Close 按钮时,生成如下代码:
new SpellCheckDlg().getClose().click();
- 找到对话框对应的测试对象
通过查看例 1,不难发现,对话框的标题“Spell Check”是匹配测试对象的条件之一,如果我们得到当前对话框的标题,并与每个测试对象的定义比较,找出符合条件的,便能得到对应的测试对象。获取对话框标题的方法已经在例 3 中给出,接下来需要做的是如何得到每个对话框里面定义的标题。在实际的项目里,对话框的标题往往定义在某个资源文件中,而不是像例 1 那样写死在类里面,所以很难通过分析类文件来知道具体定义信息。一个通用的解决方法是在对话框的基类里增加一 getDefinedCaption
方法,如例 7 所示。有了这个方法,便能以如下步骤找到对话框的定义:
- 遍历测试对象里对话框定义的类文件,找出所有的类名。一个简单的实现方法,就是遍历项目文件夹下的每个文件,通过分析文件和路径名称,找出对话框对应的类名。也可以使用
Thread.currentThread().getContextClassLoader().getResource(String)
方法,好处是适用于使用 jar 包的情况。具体实现留给读者,本文不再累述。 - 实例化每个类,并反射调用
getDefinedCaption
得到标题(例 8) - 将上一步得到的标题与实际显示的对话框比较,直到得到正确的类
例 7. 对话框基类的定义
public class NDialog{ public NDialog(String caption){ sCaption = caption; // 省略其余不相关代码 } public getDefinedCaption(){ return sCaption; } }
例 8. 实例化 NDialog 类和反射调用 getDefinedCaption
Class<?> cls = Class.forName(sDialogClassName); Object dlg = cls.newInstance(); String sCaption; if(dlg instanceof NDialog){ sCaption = ((NDialog)dlg).getDefinedCaption(); }
- 获取选中控件的 Getter
一个对话框中的 UI 控件,在 IBM 框架的应用对象里,被定义成一个个 get
开头的方法,通常叫做 getter
。录制器找到对话框之后,接下来需要找到点击到的控件对应的 getter
函数。这一点可以通过两步做到:
- 如例 9 所示,遍历对话框的每个方法,对于返回类型是控件的方法,调用它的
getRect
和getDefinedClassName
方法。将得到的数据和方法名一起,保存到一个 list 中备用。 - 将 list 的每个元素,与当前点击到的控件属性对比,如果同时符合下面两个条件,就可以认为
getter
与点击的控件对应:- 点击发生在控件显示的区域内
- 控件类型一致
例 9. 遍历对话框的 getter 方法,得到每个控件实际显示信息
List list = new Vector<Object[]>(); // 存放控件信息 //dlg 是上一步找到的对话框的实例 Method[] methods = dialog.getClass().getMethods(); for(Method m: methods){ String type = m.getGenericReturnType().toString(); // 返回类型必须是控件 if(!type.contains("appobjects.widgets"))continue; //getter 方法名称 String sGetterName = m.getName(); Object control = m.invoke(dialog); //NControl 是控件的基类 if(control instanceof NControl){ // 只处理屏幕上显示的控件 if(((NControl)control).exists()){ // 控件实际显示大小和屏幕上的位置 Rectangle rect = ((NControl)control).getRect(); // 控件类型,如 Button 和 Edit String sClassName = ((NControl)control).getDefinedClassName(); // 将控件名字,位置信息和类名放入 list 中 list.add(new Object []{sGetterName, rect, sClassName}); } } }
- 记录事件
录制得到点击发生的对话框和控件之后,还需要录制事件类型,比如鼠标单击和输入文字。最简单的做法,便是把系统事件与控件方法做一个映射,如例 10 所示。对于复杂的操作,例如选择下拉菜单,则需要针对实际情况处理,本文不再详述。
例 10. 系统事件与控件方法的对应
- 单击 --
click()
- 双击 --
doubleClick()
- 右击 --
click(RIGHT)
- 输入文字 --
setText(str)
- 输入快捷键 --
typekeys(str)
组合成完整的录制器
上面已经介绍了实现一个录制器涉及的主要技术,下面让我们看一看如何进一步设计出一个脚本录制器的框架,并在此框架基础上扩展出符合自己项目需要的 RFT 脚本录制器。框架把一般用户开发脚本录制器的相同部分抽象出来,将这些相同的部分封装固化在框架内部。这样,当用户需要开发自己的脚本录制器时,只需要在框架基础上继续开发,从而加快脚本录制器的开发速度。
录制器框架设计的主要目标是使框架自身的功能完全内敛,并且为用户的自定义开发预留出接口。为了更好的封装框架的功能,使之与用户自定义的扩展相分离,我们可以参考如图 6 所示的录制器框架的线程关系图。系统分为三个核心线程:主线程、监视线程和处理线程。主线程负责维护一个事件队列;监视线程是系统的前端,它负责收集有效的 UI 事件并将其放入到事件队列中;处理线程是系统的后端,它不断的从事件队列中取出 UI 事件,根据事件的信息生成相应的脚本代码。
图 4. 录制器框架的线程关系图
在系统的逻辑结构上,基于 MVC 思想,系统可划分为三个主要的包:Controller 包、View 包、和 Model 包,如图 5 所示。Controller 包的职责是系统的总体调度协调,它定义了框架运行的时序关系,另外它还维护一个可全局访问的消息事件队列。Model 包包含了所有的识别器,它是整个系统的处理中心,事件队列中所有的消息事件最终通过某个识别器来处理。View 包承担两方面的任务,一是显示系统界面,二是监听所有 UI 事件,将这些 UI 事件包装后放入到事件队列中。
图 5. 系统的包结构图
图 5 定义了一个录制器的主要部件,框架包含 Controller 包及 View 包的全部功能及 Model 包中的识别器工厂类,Model 包中的识别器库则由用户根据自己项目的需要扩展开发。在系统运行时,框架可以检测到所有的识别器,并将它们加载到程序之中。对于识别器库的开发,考虑到有些识别器是针对一类界面元素,也有些识别器只针对一个具体的界面元素,为了使框架无差别的对待这两种识别器,框架采用了一个组合模式来实现这个目的。如图 6 所示为应用组合模式的类图。
图 6. 识别器库的组合模式类图
接口 IdentifierComposite
是框架定义的识别器接口,框架还实现了一个 RootIdentifer 类作为所有识别器的要根类,它主要实现了框架管理识别器库的方法,用户自定义扩展的所有识别器均应直接或间接的继承自 RootIdentifier 类。
当用户开发识别器时只需实现 IdentifierComposite 中的两个方法 accept()
和 generateCode()
即可,框架中的识别器工厂类负责将所有识别器加载到内存当中,在处理事件时,框架会找到合适的识别器来处理。识别器通过 accept()
告诉框架它能否处理某个事件,当框架找到处理当前事件的正确识别器后,调用该识别器的 generateCode()
方法来得到生成的脚本代码,显然对于这两个方法的调用,框架会向识别器传递必要的参数。
如图 7 所示是 Lotus Notes 自动化测试系统中创建的录制器的界面,录制器在启动时会加载识别器库,待加载完成后,按钮“Loading”会变为“Start”,用户即可以开始使用此录制器来录制 UI 操作。待录制完成后,点击“Copy to Clipboard”按钮即可将生成的脚本放入系统剪贴板中,用户只需再在编辑器中按“Ctrl+V”即可得到生成的测试脚本代码片段。
图 7. 录制器界面
至此,我们已可以构造出一个简单的适合项目的脚本录制工具。在实际项目中,除了窗口之外,我们还需要更细致的支持其他界面元素,比如菜单、系统对话框等等,而这些支持都可以逐步的加入到工具中,不断增强工具的适用性。
支持跨平台
如果我们的产品在未来需要跨平台运行,那么我们的自动化测试系统也需要提供相应的支持。同一个应用程序在不同的平台上运行往往会表现出一些差异性,既包括界面渲染上不同,也有功能展示和用户体验上的区别。在对跨平台应用程序进行自动化测试时,这些差异性通常会影响测试脚本的复杂度和编写维护的难度,当然也会增加工作量。
作为同时支持 Windows、Mac、Linux 平台的大型应用,Lotus Notes 相关的自动化测试框架和测试用例也需要考虑跨平台的影响,在此,我们可以一起了解一下在 Lotus Notes 项目中的一些最佳实践。
使用 ibm.util.Platform 类中的方法判别和区分平台相关的代码
在 Lotus Notes 的自动测试框架中,ibm.util.Platform 类专门用来判断系统平台,提供了一系列例如 isMac()
、isWindows()
、isLinux()
等判断基础系统平台的方法,以及类似 isMacLion()
、isWin7()
、isUBUNTU()
等判断系统版本的方法。
在测试框架和测试脚本中,所有平台相关的代码都需要用以上平台和版本判断代码区分开,如例 11 所示:
例 11. 区分平台相关代码
if (ibm.util.Platform.isWindows()) { processes = processesWindows(); } else if (ibm.util.Platform.isLinux()) { process = processesLinux(); } else if (ibm.util.Platform.isMac()) { process = processesMac(); } else { Process = false; }
将平台相关性封装在自动化测试框架中底层
这里所说的底层指的是 IBM 框架中的任务层和应用对象层。相对测试用例层而言,这两层更加稳定,应用范围也更广,在编写和维护测试用例的时候也就不必过多关注于不同平台的差异性,可以减少与测试无关的细节,在不同平台上共用相同的测试用例,减少相当的测试用例的复杂度与工作量。
比如,在一个测试用例中需要在打印对话框中选中一个名为“Selected documents”的选项,而打印对话框是系统对话框,在 Windows 和 Mac 平台上是不同的,所以选项的位置和选择的方式都会有所区别。对于这个操作,我们可以在测试用例中用如下代码完成:
Menu.mPrint.pick(); if (Platform.isMac()) { new OsPrintMac().getNotesOption().click(); } new DlgPrintView().getDeskPrintSeldocs().click();
不过更好的方法是将判断 Mac 系统和点击“Notes Options”的代码发到任务层的 PrintTask 中,而测试用例的代码就可以这样写:
PrintTask.openPrintViewDialog(); new DlgPrintView().getDeskPrintSeldocs().click();
看起来更简洁了,也更易懂了,对吧?
另一个具有普遍意义的例子是关于快捷键。在 Windows 和 Linux 系统中,很多快捷键需要用到 Ctrl 键,而在 Mac 系统中,则要用 Cmd 键代替。比如要拷贝所有文字,我们可以这么写:
getScreen().inputKeys(Platform.isMac() ? "&a" : "^a"); getScreen().inputKeys(Platform.isMac() ? "&c" : "^c");
而更好的方法是将 Mac 系统相关的代码放到 IShortCuts 对象中(位于应用对象层),然后在测试用例中这样写:
getScreen().inputKeys(IShortCuts.scSelectAll); getScreen().inputKeys(IShortCuts.scCopy);
添加适当的延时
Lotus Notes 在 Linux 和 Mac 系统中运行要比在 Windows 系统中慢一些,因此在编写一些测试用例时要注意适当添加延时操作,尽量使用exists(timeout)
和 waitForNonExistence(timeout)
方法,比如:
Menu.mDatabaseOpen.pick(); new OsBrowse().getFileName().setText(sDB);
这些代码在 Windows 系统中能够正确地被执行,然而在 Linux 和 Mac 系统中却经常出问题,日志中会记录找不到控件的错误。实际上只是因为对话框弹出较慢,在选择菜单项后添加 sleep(time)
就可以解决了,实际上推荐在这里使用 exists(timeout)
和waitForNonExistence(timeout)
方法,这些在之前的系列文章中已经介绍过,这里就不多说了。
Menu.mDatabaseOpen.pick(); if(new OsBrowse().exists(LocalSettings.giSTO)){ new OsBrowse().getFileName().setText(sDB); }else{ Logger.logError("Open Database Application dialog is not found"); }
另外按键输入的性能在 Linux 和 Mac 系统中运行也要比在 Windows 系统中慢一些,例如:
textfield.setText("Hello");
有可能因为 Shift 键释放较慢而得到“HEllo”。解决的方法是,除非必要,全部使用小写字符串。并且如下调整 RFT 延时属性:
图 8. 调整 RFT 延时属性
注意大小写字符
在 Linux 系统和 Mac 系统中,文件路径和文件名都是大小写敏感的,这与在 Windows 平台中是不同的,在测试用例中涉及到文件名和文件路径时,一定要注意。