级别: 初级 刘 哲, 软件工程师, IBM 邢 静, 软件测试工程师, IBM 段 雪飞, 高级软件工程师, IBM
2009 年 5 月 31 日 Eclipse 富客户端应用程序(Rich Client Platform,RCP)通常运行在多种平台之上,因此我们需要对其进行跨平台测试。本文将详细介绍如何使用 Rational Functional Tester(RFT)对 Eclipse RCP 应用程序实施跨平台自动化测试,以及如何处理本地控件和平台差异给测试带来的问题。
引言
Eclipse 富客户端应用程序(Rich Client Platform,RCP)通常运行在多种平台之上,因此我们需要在多种平台上对其进行测试。IBM Rational Functional Tester 作为一款优秀的自动化测试工具,具备了对 Eclipse RCP 应用程序进行自动化测试的能力。文章将主要介绍使用 RFT 实施跨平台测试的经验以及如何解决一些可能会遇到的问题。本文假设你对 RFT 和 Eclipse RCP 有一定的使用经验和认识,并且熟悉 JNI,SWT,GTK 等技术。
RFT 能够帮助我们什么
Rational Functional Tester 提供了跨平台的测试脚本(Java 或者 Basic)和开发环境(基于 Eclipse 的 IDE),统一的 API 接口和对 Eclipse 应用的全面支持。你能够在 windows 下录制一个脚本然后在 linux 上进行回放。在测试之前,请参考 RFT 的帮助文档来对所要测试的 Eclipse RCP 进行必要的配置。这样 RCP 应用程序会被安装一个 RFT 提供的插件,用来捕捉 SWT 控件,记录用户操作,添加验证点,并生成测试脚本。
RFT 能够识别所有标准的 SWT 控件,并将其映射到对应的 TestObject 类上。Eclipse 应用程序往往包含一些自定义的控件,针对这些控件,可以使用 RFT 提供 TestObject.getProperty 和 invoke 方法来处理。本质上这两个方法利用了 Java 反射机制,因此你可以轻松获取被测对象中的任何信息和调用其任何方法。当然这种方法适用于人工编制脚本而不适用使用脚本录制这种情况。RFT Proxy SDK 可能是一个更好的方案。使用 Proxy SDK 能够为自定义的控件添加更加丰富的行为和更加准确的识别信息,同时也能正确的被 RFT 识别和进行录制。读者可以阅读相关文章来获得更多信息。
对 Eclipse RCP 实施跨平台自动化测试中的麻烦
尽管 RFT 能够支持 Windows 和 Linux 平台,完全可以满足对 Eclipse RCP 在这两个平台上进行自动化测试的基本要求,但是在具体项目的实践中,我们发现不得不面临一些其它麻烦,主要有两个方面:
-
RFT 无法识别一些特殊的 GUI:RFT 能够识别和操作所有标准的 SWT 控件,但是 RCP 应用程序为了和系统风格保持一致,往往还会使用操作系统本地控件,比如文件对话框,打印对话框,颜色选择框,字体选择框等等,RFT 无法处理这些控件。另外一些 RCP 会集成一些遗留应用程序。这些程序使用了自己的图形库而不是 SWT 库,显然 RFT 也将无能为力。这影响了自动化测试覆盖面。
-
被测的 RCP 在不同平台下存在差异:主要包括快捷方式,命令,UI 字符串甚至是操作方式等方面的差异。这些差异经常导致同一套脚本无法在多个平台下使用。如何有效地屏蔽这些差异是降低开发和维护成本的一个重要因素。
文章接下来的部分将详细说明如何处理这些问题。
扩展 RFT 以处理本地控件
虽然 RFT 提供的 IWindow 接口能够识别操作系统相关的控件,但是它存在着很大的局限性,使其很难被应用到具体实践中去。IWindow 支持的 GUI 操作和属性十分有限,仅支持鼠标键盘操作,获得控件是否显示,是否具有焦点,而无法判断一个复选框是否被选中,无法获得一个列表框中的所有条目,以及控件的提示内容等等。特别是在 Linux 下,RFT 并不能获取所有控件所对应的 IWindow。
处理 Windows 本地控件
RFT 本身是能够通过 Win Domain 来识别 Windows 控件,不过默认情况它是不会自动开启。你可以通过清单 1 中的代码使用 Win Domain 来访问 GUI 控件。getDialogTestObject 将在一个对话框上启用 Win Domain 并返回代表这个对话框的 TestObject, 然后就可以通过 TestObject.find 方法来查找所需的控件。testMain 中的代码显示了如何定位一个位于打开对话框中的 checkbox。就像 Java Domain 中的 TestObject 一样,你可以获得其属性或者进行某种操作。
清单 1
public TestObject getDialogTestObject(String caption) {
if (rootCaption != null) {
RootTestObject root = RootTestObject.getRootTestObject();
IWindow[] wnds = root.getTopWindows();
for (int i = 0; i < wnds.length; i++) {
if (wnds[i].getText().equals(caption)) {
Long hWnd = new Long(wnds[i].getHandle());
// 在该窗口上启用 win domain
TestObject[] children = root.find(RationalTestScript.atChild(".hwnd",
hWnd, ".domain", "Win"));
if (children.length > 0) {
// 释放对其他控件的引用 .
for (int j = 1; j < children.length; j++)
children[j].unregister();
return children[0];
}
}
}
}
return null;
}
public void testMain(Object[] args) throws Exception {
TestObject dialog = getDialogTestObject("Open");
// 查找一个标签为 "Link" 的复选框
TestObject[] wins = ret.find(atDescendant(".class", ".Checkbutton",
".text", new CaptionText("Link")));
//TODO other operations
}
|
如果执行过程中遇到异常”com.rational.test.ft.UnableToHookException: Internal Error, please contact customer support: An error occurred hosting the.NET CLR within the process”,你需要在测试机上安装 .Net Framework,RFT 依赖它来访问 Windows 控件。
扩展 RFT 支持 Linux 本地控件
在 Linux 上,RFT 没有类似 Win Domain 的域来识别系统本地控件。IWindow 也仅能识别部分本地控件。我们需要扩展 RFT 来弥补这个缺陷。Linux 有着非常多的图形库,SWT 有 GTK 版也有 Motif 版,这里仅介绍如何识别 GTK 控件的思路。
在 Linux 中无法直接访问和操作另一个进程中的控件,因此需要在被测软件 (AUT) 的进程中放置一个钩子。在 RFT 中通过进程间通讯机制来连接钩子进而达到自动测试的目的。实现进程间通讯可以用管道或者网络套接字,完全自己实现这些底层问题成本高时间长。实际上可以借助 RFT 的 Java Domain 来巧妙地规避这个问题。RFT 跨进程调用 Eclipse Enabler 中的 Java 类来访问 SWT 控件。我们可以使用 JNI 来实现钩子(GTK Hook),将其封装在一个 Java 类中。在 RFT 端,也通过 Eclipse Enabler 调用这个类,从而避免自己实现的跨进程通讯,如图 1 所示。
图 1. 结构图
GTK Hook 的实现
在 RFT IDE 中,选择 File -New – Other,然后选择 Class 创建一个名为 GtkHook 的 Java 类。在该类中定义存取 GTK Widget 的 native 静态方法,如代码清单 2 所示,每个 native 方法的第一个参数都为窗体的 handle。通过 RFT 的 IWindow 很容易获得顶层窗体的 handle。然后我们可以通过 getChildren 得到所有的子控件。getProperty 用于得到控件的某一个属性 ( 请参考 Gtk 手册获得控件所支持的属性列表 )。getScreenRectangle 可以获得控件在屏幕上的位置和大小。getClassName 能得到 Gtk 控件的类型。
清单 2. GtkHook 的 Java 定义
package test;
import java.awt.Rectangle;
public class GtkHook {
static {
System.loadLibrary("gtkhook");
}
// 获得 GTK widget 的一个属性
private static native Object getProperty(long hwnd, String propertyName);
// 获得 GTK widget 的所有子窗体 handle
private static native long[] getChildren(long hwnd);
// 获得 GTK widget 的类型
private static native String getClassName(long hwnd);
// 获得 GTK widget 在屏幕上的区域
private static native Rectangle getScreenRectangle(long hwnd);
}
|
打开终端运行命令:
javah test.GtkHook
将生成 C 代码的头文件。创建一个 .c 文件,通过 XLib 和 GTK+ API 实现所有 JNI 方法。每个方法的第一步我们都需要通过 Native Window Handle 来查找相对应的 GtkWidget,可通过代码清单 3 来实现。
清单 3. 查找 GtkWidget
// 通过 handle 来查询一个 GTK widget。如果找不到,抛出一个运行时异常
GtkWidget* GetGtkWidget(JNIEnv *env, jlong handle) {
GdkWindow* window = gdk_window_lookup((GdkNativeWindow) handle);
if (window) {
gpointer user_data = NULL;
gdk_window_get_user_data(window, &user_data);
return GTK_WIDGET(user_data);
} else {
jclass excepCls = (*env)->FindClass(env, "java/lang/RuntimeException");
(*env)->ThrowNew(env, excepCls, "WidgetNotFound");
return NULL;
}
}
|
获得 GtkWidget 指针以后,就可以很容易的存取该 widget 的各种属性和调用其各种方法。比如清单 4 为 getProperty 的实现,它读取 widget 所支持的属性并将其值转换为相应的 Java 类型。
清单 4. getProperty 的实现
// 获得一个 GTK widget 的属性
JNIEXPORT jobject JNICALL Java_test_GtkHook_getProperty
(JNIEnv *env, jclass cls, jlong handle, jstring key) {
GtkWidget *widget=GetGtkWidget(env, handle);
if (widget) {
const char *cKey = (*env)->GetStringUTFChars(env, key, 0);
GParamSpec *pSpec = g_object_class_find_property(G_OBJECT_GET_CLASS(widget), cKey);
jobject ret = NULL;
if (pSpec) {
switch(G_PARAM_SPEC_VALUE_TYPE(pSpec)) {
case G_TYPE_INT: {
// 获得一个整形类型的属性
jint intValue = 0;
g_object_get(widget, cKey, &intValue, NULL);
jclass intClass = (*env)->FindClass(env, "java/lang/Integer");
jmethodID intConsMID = (*env)->GetMethodID(env, intClass, "<init>", "(I)V");
ret = (*env)->NewObject(env, intClass, intConsMID, intValue);
break;
}
// 实现对其它类型属性的读取。略。。。
}
}
//Release strings
(*env)->ReleaseStringUTFChars(env, key, cKey);
return ret;
}
}
|
其它方法的实现读者可以在本文的附件中获得。值得注意的是,涉及到 GUI 的 API 必须在主线程中被执行,否则会使程序崩溃。因此我们应该将所有对 GTK API 的调用通过 g_idle_add 把它们放入主线程中去执行,在远程调用线程中等待结果,如 getScreenRectangle 方法。本文提供的源码仅实现了部分基本的功能,如有需要读者可自行添加。
完成所有 JNI 方法的实现后,我们需要将其编译成动态连接库。在终端里执行命令:
gcc test_GtkHook.c -shared -I. -I/opt/IBM/SDP70/jdk/include -I/opt/IBM/SDP70/jdk/include/linux -o libgtkhook.so `pkg-config --cflags --libs gtk+-2.0`
配置 GTK Hook
为了使 GtkHook 能够被 RFT 远程调用,需要将 test/GtkHook.class 文件加入到 RFT eclipse enabler plugin(位于 RFT 安装目录 \FunctionalTester\EclipseEnabler\plugins)中的 rational_ft_bootstrap.jar,并把生成的 libgtkhook.so 复制到插件的目录下。部署该插件到 RCP 应用程序后,RFT 就能够访问程序中的 GTK Widget。
在 RFT 脚本中使用 GTK Hook
在 RFT 我们需要通过 DomainTestObject.invokeStaticMethod 来远程调用 test.GtkHook 类间接地调用 GTK API。使用 GtkHook 的步骤:
第一步,通过 RationalTestScript.getTopWindows() 得到 widget 所在的窗体句柄
第二步,调用 GtkHook 的 getChildren 遍历窗体中所有的 widget
第三步,得到期望 widget 的句柄,调用 GtkHook 获取其属性或进行操作。
例如我们可以通过代码清单 5 来要获得如图 2 所示对话框中 Toggle Button 的状态。
图 2. Linux中的文件对话框
清单 5. 如何使用 GtkHook
// 获得窗口的 handle
public long getWindowHandle(String caption) {
IWindow[] topWins = RationalTestScript.getTopWindows();
for (int i = 0; i < topWins.length; i++) {
// 跳过 decorator 窗体
IWindow realWin = topWins[i];
IWindow[] wrappedWins = topWins[i].getChildren();
if (wrappedWins.length == 1)
realWin = wrappedWins[0];
if (new Regex(caption).matches(realWin.getText()))
return realWin.getHandle();
}
throw new ObjectNotFoundException("Can't find the window");
}
// 远程调用 GtkHook 中的方法
public Object invokeRemote(String method, String signature, java.lang.Object[] args) {
return getJavaDomain().invokeStaticMethod("test.GtkHook", method, signature, args);
}
public void testMain(Object[] args) throws Exception {
long saveAsDlgHwnd = getWindowHandle("Open");
// 获得 "Open" 对话框的所有子窗体的 handle
long[] children = (long[]) invokeRemote("getChildren", "(J)[J", new Object[]{
new Long(saveAsDlgHwnd)});
for (int i = 0; i < children.length; i++) {
String type = (String) invokeRemote("getClassName", "(J)Ljava/lang/String;",
new Object[]{new Long(children[i])});
// 判断 Widget 的类型
if ("GtkToggleButton".equals(type)) {
// 输出 ToggleButton 的状态
Boolean checked = (Boolean) invokeRemote("getProperty",
"(JLjava/lang/String;)Ljava/lang/Object;", new Object[] {new Long(children[i]),
"<a
href="
http://library.gnome.org/devel/gtk/2.14/GtkToggleButton.html#GtkToggleButton--active"
> active</a>"});
System.out.println("GtkToggleButton checked: " + checked);
}
}
}
}
|
本地控件的封装类
由于不同平台下,操作本地控件的 API 和方式存在差异,需要进行必须要的封装以提高易用性。为常见的控件定义一个代理类并实现统一的接口,如 IButton,IListBox,IComboBox,ICheckBox,IRadioBox 等等,如图 3 所示。控件类的构造函数以控件所在的窗体的标题以及在同类型控件中的索引为参数作为识别信息。
图 3. 封装控件API
(查看大图)
在使用控件时,测试脚本仅需要访问接口。在使用 IBM 三层结构构建自动化测试项目时,可以如下定义本地控件。
// 获得 Save As 对话框中的第一个 ListBox
public IListBox getTypeListBox() {
if (OS.isWindows()) {
return WinsListBox("Save As", 0);
} else if (OS.isLinux()) {
return GtkListBox("Save As", 0)
} else {
return null;
}
}
|
降低平台差异带来的影响
为了屏蔽差异以提高维护性和开发效率,我们可以使用分层结构来组织自动化测试工程,如图 4 所示。
图 4. 工程的层次结构
这样的结构有以下特点:
-
使用 IBM 三层结构来构建自动化工程,将“做什么”与“如何做”分离开来,提高代码的重用性和维护性。
-
把所有在不同平台下 GUI 的差异封装到 appobjects 层。当上层从该层中获得一个 GUI Object 时上层不需要知道它所操作的 GUI Object 在不同平台下是不一样,它所面对的是一致的接口。
-
把所有在不同平台下的操作差异封装到 task 层。有些情况下,完成一项任务,在不同的操作系统会有不同的流程。这个时候就需要把流程封装成一个 task,使 testcase 层使用这些 task 时不要关心操作系统带来的差异。
-
统一管理易变的资源(GUI 字符,快捷键,命令等等)。
我们建议先为通用资源建立一个存储文件,再为不同的平台建立不同的文件来存储平台特有的资源。例如可以命名这些资源文件为:
shortcut.properties // 定义通用快捷键
shortcut_wins.properties // 定义 Windows 特有的快捷键
shortcut_linux.properties // 定义 Linux 特有的快捷键
shortcut_mac.properties // 定义 Mac 特有的快捷键
然后定义一个 Java 工具类来读取它们。该类优先加载当前运行的平台所对应文件中的资源,如果没有对应的资源,则使用通用的资源。这有点像 Java 中 java.util.ResourceBundle 对国际化资源的处理方式。在脚本中通过一个唯一的 key 来使用这些资源。这样做的好处是:
-
当这些资源变化时,你只需要更新相应的资源文件,便于维护。
-
你不再需要在脚本中使用大量的判断来处理不同平台的不同。
例如,可以用清单 6 中的代码在自定义的 Script Helper 里面覆盖 IScreen.inputKeys。脚本中使用 inputKeys("Hello${Undo}${Redo}${SelectAll}${Copy}") 来输入一串文字,尝试 Undo 和 Redo 后将其复制到剪切板中,其中的 ${Undo},${Redo},${SelectAll} 和 ${Copy} 都是定义在资源文件中的快捷键。脚本使用相同的 Key 来使用快捷键,无需关心它们在不同平台是否一样。
清单 6 覆盖 IScreen.inputKeys
public static void inputKeys(String input) {
Pattern pattern = Pattern.compile("\\$\\{([^\\}]+)\\}");
Matcher matcher = pattern.matcher(input);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
// 替换 ${xxx} 为资源文件中的 keys
String rep = Resource.get(matcher.group(1), matcher.group()).replace("\\",
"\\\\").replace("$", "\\$");
matcher.appendReplacement(result, rep);
}
matcher.appendTail(result);
getScreen().inputKeys(result.toString());
}
|
Undo,Select All,Copy 快捷键在 Windows 和 Linux 下相同,在 shortcut.properties 中定义:
Undo=^z
SelectAll=^a
Copy=^c
|
Redo 快捷键在 Windows 和 Linux 下不相同,在 shortcut_linux.properties 中定义:
在 shortcut_wins.properties 中定义:
结束语
Lotus Symphony 是一个跨平台的基于 Eclipse RCP 的办公软件,支持 Windows, Linux 和 Mac OS X 平台。其在 GUI 上使用了大量本地控件,并在不同平台下存在一些差异,这给自动化测试带来了挑战。本文阐述了我们在该项目中解决这些问题的技术方案,通过上述方法极大提高了自动化测试的应用范围和平台覆盖率,降低了开发测试用例的难度。希望我们的方法也能对读者有所帮助。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 示例代码 | GtkHook.zip | 25 KB | HTTP |
|---|
参考资料 学习
获得产品和技术
讨论
作者简介  | |  | 刘哲,是 IBM 中国软件开发中心的一名 IBM Lotus 软件工程师。喜欢 Java,自动化测试和 Web 开发。 |
 | |  | 邢静,IBM CDL 的一名软件测试工程师, 从事 IBM Lotus Symphony 自动化测试工作。 |
 | |  | 段雪飞,2003 年加入 IBM,高级软件工程师,目前负责 IBM Lotus Symphony 自动化测试工作。 |
对本文的评价
|