使用 XPath 在 Rational Functional Tester 中动态识别对象
http://www.ibm.com/developerworks/cn/rational/r-cn-rftxpathdynobj/
IBM Rational Functional Tester(RFT)是一个非常灵活的自动测试工具。通常情况下,用户可以通过 ObjectMap 来存储 GUI 对象的识别信息。ObjectMap 中的测试对象必须有严格的层次结构,位置或者某个不变的属性,非常适用于一些静态的不易变化的 GUI。然而被测程序总是会包含一些动态的 GUI 对象,这些对象的层次或者属性在运行时具有不确定性。RFT 提供了一些 API 来处理这些对象,比如 TestObject 的 find 和 getChildren 方法。这些 API 能够解决查找动态变化的 GUI 对象的问题,但是直接使用它们会增加脚本的逻辑复杂度,提高了开发难度,同时使得脚本也不易维护。本文将介绍如何引入 XPath 作为测试对象的识别语言,从而简化动态对象的识别问题。
RFT 中的 Find 方法的输入参数是各种查询条件或这些条件的组合。可用的查询条件包括:atProperty、atChild、atDescendant、atList 等等。
针对复杂的 GUI 对象,我们不得不通过大量的 Java 或 VB 代码来实现这些条件组合。如果能用一个比较简单的字符串来表示复杂的查询条件,肯定会受到脚本开发者的欢迎。于是我们想到了在 XML 技术里面的 XPath。XPath 被证明是一个非常灵活和简便易学的轻量级查询语言,广泛应用于在 XML 文档中查询文档中特定文本、元素和属性。我们把它应用于在 RFT 中查询 GUI 对象将是一个不错的想法。
首先让我们通过几个实例来比较一下使用 XPath 和直接使用 RFT API 来识别对象的区别。当用户测试一个 eclipse 应用程序时,他需要动态查找一个按钮,于是使用 RFT API 写下如下代码:
find(atList(atChild(".class", "org.eclipse.swt.widgets.Shell", ".captionText", "Hello"),
atChild(".class", "org.eclipse.swt.widgets.Button", "text", "OK")));
|
使用 XPath 时 , 如下:
findByXPath(
"org.eclipse.swt.widgets.Shell[@captionText=
'Hello']/org.eclipse.swt.widgets.Button[@text='OK']");
|
XPath 使代码更加简洁,在更加复杂的查询条件下更加明显。比如,动态查找一个按钮,它总是所有按钮中的最后一个。可以使用如下代码:
findByXPath(shellTO, "org.eclipse.swt.widgets.Button[last()]") |
而如果不用 XPath 则需要:
TestObject[] tos = find(atChild(".class", "org.eclipse.swt.widgets.Button"));
TestObject expectedTestObject = tos.length == 0 ? null : tos[tos.length - 1];
|
XPath 中还能支持逻辑,算数运算以及函数,可以将复杂的多重的查询条件用一个字符串来表示,特别值得一提的是它还可以方便地根据子孙对象的属性来识别对象,使测 试脚本更加精炼。如查找一个对话框,它的标题为”Error”并且它的里面包含一个内容为”An error occurs”的 Label:
findByXPath(“org.eclipse.swt.widgets.Shell[@text='Error'
and descendant:: org.eclipse.swt.widgets.Label/@text='An error occurs']”);
|
而如果不用 XPath 则需要:
TestObject[] tos = find(atChild(".class",
"org.eclipse.swt.widgets.Shell", "text", "Error"));
TestObject expectedShell = null;
for (int i = 0; i <tos.length; i++) {
TestObject[] labels = tos[i].find(atDescendant(".class",
"org.eclipse.swt.widgets.Label", "text", "An error occurs"));
if (labels.length > 0) {
expectedShell = tos[i];
break;
}
}
|
显然直接使用 RFT API 要复杂的多。随着被测应用程序的不断更新,识别 GUI 对象的逻辑也要随时更新,就不得不修改大量的脚本代码,这大大增加了维护成本。
接下来介绍如何实现这个想法。应用程序中的 GUI 对象是一个层次结构,一个对象有父对象也有子对象形成一颗树。把 GUI 对象看成 XML 中的元素,对象的类名就是 XML 中的元素标签,对象的属性就是 XML 中元素的属性。
我们以 eclipse 中”Add Bookmark”对话框 ( 图 1) 为例,它对应的 XML 表示如清单 1 所示。GUI 对象的父对象和子对象可以由 TestObject.getParent() 和 getChildren 得到。TestObject.getObjectClassName() 和 getProperty 可以获得对象的类名和属性。我们需要做的就是将 XPath 转换为对 RFT 底层 API 的调用。
<org.eclipse.swt.widgets.Shell captionText="Add Bookmark"> <org.eclipse.swt.widgets.Composite> <org.eclipse.swt.widgets.Composite> <org.eclipse.swt.widgets.Label text="Enter Bookmark name:"/> <org.eclipse.swt.widgets.Text priorLabel="Enter Bookmark name:"/> </org.eclipse.swt.widgets.Composite> <org.eclipse.swt.widgets.Composite> <org.eclipse.swt.widgets.Button text="Cancel"> <org.eclipse.swt.widgets.Button text="OK"> </org.eclipse.swt.widgets.Composite> </org.eclipse.swt.widgets.Composite> </org.eclipse.swt.widgets.Shell> |
解析 XPath 表达式有些困难,不过 Jaxen 可以帮助我们,它是一个开源的 XPath 引擎,提供了一种适配器机制以支持用 XPath 查询非 XML 模型。这一特性正要我们想要的。
要想在 Jaxen 中适配 RFT 的模型,第一步要做的是实现 Navigator 接口,如果想提高效率可以实现NamedAccessNavigator接口。Jaxen 提供一个 Navigator 接口的默认实现 DefaultNavigator。定义我们的 RFTNavigator 类,使其扩展 DefaultNavigator 并实现NamedAccessNavigator接口。这里介绍一些关键方法的实现,完整代码请参见下载部分的示例源码。
清单 2 显示了如何转化 TestObject 的属性为 XML 中的属性节点来实现 getAttributeAxisIterator 方法。需要注意的是 TestObject 的有些属性名以点开头,这不符合 XML 命名规则,可以用下划线作为转义符进行转义。
清单 2. getAttributeAxisIterator 的实现
/**
* Get the attributes of the element
*/
public Iterator getAttributeAxisIterator(Object contextNode) {
TestObject to = (TestObject) contextNode;
Hashtable props = to.getProperties();
List<Property> list = new ArrayList<Property>(props.size());
Iterator i = props.entrySet().iterator();
for (; i.hasNext();) {
Entry e = (Entry) i.next();
String key = (String) e.getKey();
if (key.startsWith("."))
key = "_" + key;
list.add(new Property(key, e.getValue()));
}
return list.iterator();
}
/**
* Get the attributes of the element according to the attribute name
*/
public Iterator getAttributeAxisIterator(Object contextNode,String localName,
String namespacePrefix, String namespaceURI) {
TestObject to = (TestObject) contextNode;
String keyEscaped = localName;
if (localName.startsWith("_."))
keyEscaped = localName.substring(1);
Object value = to.getProperty(keyEscaped);
Property prop = new Property(localName, value);
return new SingleObjectIterator(prop);
}
|
清单 3 显示了如何转化 TestObject 为 XML 中的元素节点来实现 getChildAxisIterator。需要注意的是当 TestObject 为 DomainTestObject,应用 getTopObjects 来得到子节点。
清单 3. getChildAxisIterator 的实现
/**
* Retrieve an Iterator matching the child XPath axis
*/
public Iterator getChildAxisIterator(Object contextNode) {
if (contextNode instanceof TestObject) {
TestObject[] children = null;
if (contextNode instanceof DomainTestObject) {
children = ((DomainTestObject) contextNode).getTopObjects();
} else {
children = ((TestObject) contextNode).getChildren();
}
List<TestObject> list = Arrays.asList(children);
return list.iterator();
}
return JaxenConstants.EMPTY_ITERATOR;
}
/**
* Retrieve an Iterator matching the child XPath axis according to the element name
*/
public Iterator getChildAxisIterator(Object contextNode,
String localName,
String namespacePrefix,
String namespaceURI) throws UnsupportedAxisException {
if (contextNode instanceof TestObject) {
TestObject[] children = ((TestObject) contextNode).find(
SubitemFactory.atChild(".class", localName));
List<TestObject> list = new ArrayList<TestObject>();
for (TestObject c : children) {
if (c.getObjectClassName().equals(localName)) {
list.add(c);
}
}
return list.iterator();
}
return JaxenConstants.EMPTY_ITERATOR;
}
|
第二步,定义 RFTXPath,用 BaseXPath 作为父类。它是使用 XPath 查询 TestObject 的入口点 , 非常简单 , 如清单 4 所示
package utils;
import org.jaxen.BaseXPath;
import org.jaxen.JaxenException;
public class RFTXPath extends BaseXPath {
public RFTXPath(String xpathExpr) throws JaxenException {
super(xpathExpr, RFTNavigator.getInstance());
}
}
|
这样我们就可以使用 XPath 来查询 TestObject 了,如下:
RFTXPath xpath = new RFTXPath(xPath); List results = xpath.selectNodes(testObject); |
在附件的源代码中提供了两个例子来演示如何用 XPath 做识别语言来测试 Java 程序和 Web 程序。demo.JavaXPathDemo 以 RFT 中预制的 ClassicsJavaB 程序作为被测程序(图 2),展示了如何使用 XPath 来识别程序中的 JTextArea 和 JButton,更重要的是读者可以看到使用 XPath 通过子控件来识别一个 JFrame 是多么容易的一件事情,代码见示例 1。
图 2 被测试的 Java 程序
示例 1. 测试 Java 程序
public void testMain(Object[] args) {
startApp("ClassicsJavaB");
sleep(3);
TestObject frame = javaframe();
//Find the first JTextArea in the first JTabbedPane
TestObject to = findByXPath(frame,
"javax.swing.JTabbedPane[1]/descendant::javax.swing.JTextArea[1]");
System.out.println("The content of JTextArea:" + to.getProperty("text"));
//Find the button named placeOrderButton2
GuiTestObject button = (GuiTestObject)findByXPath(frame,
"javax.swing.JButton[@name='placeOrderButton2']");
button.click();
//Find the JFrame which contains a button named ok-orderlogon or cancel-orderlogon
to = findByXPathWithRetry(button.getDomain(),
"javax.swing.JFrame[javax.swing.JButton[@name='ok-orderlogon'
or
@name='cancel-orderlogon']]");
System.out.println("The title of JFrame:" + to.getProperty(".captionText"));
}
|
Web 程序时常会在固定区域 ( 比如一个 DIV) 里动态创建一系列超链接,它们数量不确定,又没有 ID,但是最后一个超链永远指向一个固定页面,如图 3 所示的 HTML 源码中的 today_news DIV。要验证这最后一个超链接,用 XPath 使事情变得相当容易,代码见示例 2。
图 3 被测 Web 页面的源码
示例 2. 测试 Web 程序的代码
public void testMain(Object[] args) {
//the current user that logged in
String username = "Foo";
startBrowser("file:///C:/WebXPathDemo.html");
sleep(5);
GuiTestObject htmlBody = htmlbody();
// Find a hyperlink which text is the current user name
TestObject to = findByXPath(htmlBody,
"descendant::Html.A[@_.text='" + username + "']");
System.out.println("href property: " + to.getProperty(".href"));
// Find the last hyperlink in the <DIV> with ID "today_news"
to = findByXPath(htmlBody, "descendant::Html.DIV[@_.id='today_news']/Html.A[last()]");
System.out.println("href property of last hyperlink: " + to.getProperty(".href"));
// Directly get property value via XPath
String value = stringValueByXPath(htmlBody,
"descendant::Html.DIV[@_.id='today_news']/Html.A[last()]/@_.href");
System.out.println("href property of last hyperlink: " + value);
}
|
另外,在实际应用中我们也总结了几点技巧可供读者参考。
- 为了更加方便的使用 XPath,可以在脚本的 Super Helper Class 里面添加一个静态方法,如清单 5。
清单 5. 在 Super Helper Class 中定义 findByXPath 方法
public class ScriptHelper extends RationalTestScript {
public static TestObject findByXPath(TestObject testObject, String xPath) {
List results;
try {
XPath xpath = new RFTXPath(xPath);
results = xpath.selectNodes(testObject);
} catch (JaxenException e) {
throw new RuntimeException(xPath + " is not valid!", e);
}
if (results.size() == 0)
throw new ObjectNotFoundException("Object is not found via " + xPath);
if (results.size() > 1)
throw new AmbiguousRecognitionException(
"Multiple object are found via " + xPath);
Object obj = results.get(0);
if (!(obj instanceof TestObject))
throw new BadArgumentException("It's not TestObject found via " + xPath);
return (TestObject) obj;
}
}
|
- 给 findByXPath 加上自动重试机制,有利于提高稳定性,如清单 6。
清单 6. 在 Super Helper Class 中定义 findByXPathWithRetry 方法
public TestObject findByXPathWithRetry(TestObject testObject, String xPath) {
double delay =
(Double) getOption(IOptionName.WAIT_FOR_EXISTENCE_DELAY_BETWEEN_RETRIES);
long maxTime =
(long) (((Double) getOption(IOptionName.MAXIMUM_WAIT_FOR_EXISTENCE)) * 1000);
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < maxTime) {
try {
return findByXPath(testObject, xPath);
} catch (ObjectNotFoundException e) {
sleep(delay);
} catch (RuntimeException e) {
throw e;
}
}
throw new ObjectNotFoundException("Object is not found via " + xPath);
}
|
3. 善用 XPath 内建的逻辑操作符和函数来实现复杂的条件。
4. 由于代码里没有 unregister TestObject, 这可能占用过多的资源。读者应当注意在脚本中适时的调用 unregisterAll() 来释放资源。
5. 将测试对象的 XPath 字符串统一的保存在一个 Java 资源文件中来管理有利于脚本的维护。比如 widgets.properties:
OKButton=org.eclipse.swt.widgets.Shell[@captionText=
’Add Bookmark’]/descendant::org.eclipse.swt.widgets.Button[@text=’OK’]
BookMarkNameEdit = org.eclipse.swt.widgets.Shell[@captionText=
’Add Bookmark’]/descendant::org.eclipse.swt.widgets.Text[@text=
’Enter Bookmark name:’]
|
如果要支持多语言环境下的测试,仅需要再提供一个对应语言的资源文件,然后使用 java.util.ResourceBundle 来读取即可。比如 widgets_zh_CN.properties:
OKButton=org.eclipse.swt.widgets.Shell[@captionText=
’Add Bookmark’]/descendant::org.eclipse.swt.widgets.Button[@text=’OK’]
BookMarkNameEdit= org.eclipse.swt.widgets.Shell[@captionText=
’Add Bookmark’]/descendant::org.eclipse.swt.widgets.Text[@text=
’ Enter Bookmark name:’]
|
6. TestObject 的 classname 比较长,定义 XPath 起来比较繁琐,可以在 RFTNavigator 建立一个 Map 将 classname 映射为较短的名字。
7. 可以直接使用 XPath 来读取 TestObject 的属性值,在 Super Helper Class 中定义如清单 7 所示方法,如清单 8 所示使用这些 API。
/**
* Get a string value by xpath
*/
public static String stringValueByXPath(TestObject testObject, String xPath) {
try {
XPath xpath = new RFTXPath(xPath);
return xpath.stringValueOf(testObject);
} catch (JaxenException e) {
throw new RuntimeException(xPath + " is not valid!", e);
}
}
/**
* Get a number value by xpath
*/
public static Number numberValueByXPath(TestObject testObject, String xPath) {
try {
XPath xpath = new RFTXPath(xPath);
return xpath.numberValueOf(testObject);
} catch (JaxenException e) {
throw new RuntimeException(xPath + " is not valid!", e);
}
}
/**
* Get a Boolean value by xpath
*/
public static Boolean booleanValueByXPath(TestObject testObject, String xPath) {
try {
XPath xpath = new RFTXPath(xPath);
return xpath.booleanValueOf(testObject);
} catch (JaxenException e) {
throw new RuntimeException(xPath + " is not valid!", e);
}
}
|
String value = stringValueByXPath(htmlBody,
"descendant::Html.DIV[@_.id='w3c_home_recent_blogs']/Html.A[last()]/@_.text");
//The text property value of the hyperlink will be printed in the console
System.out.println(value);
|
8. 应用 XPath 也可以方便的选出一组符合条件的 TestObject。当需要验证多个动态对象是很方便。
本文所介绍的以 XPath 作为识别表达式,是对 RFT 动态查找 API 的进一步封装,能够适用于各种 RFT 所支持的应用程序类型。使用这种方案能够简化开发测试脚本中使用动态查找的代码量,将识别逻辑从脚本中分离出来,使脚本更加侧重测试步骤和验证的实现。由 于所有的识别信息都在一个字符串中,可以这些字符串统一存为 Java 资源文件。当被测软件的 GUI 发生变化时,也仅仅需要更改一下 XPath 识别表达式,无需修改脚本的 Java 代码,便于维护,同时也能够轻易地进行国际化测试。当然我们也应该看到这种方法不足之处,比如需要额外学习 XPath 语法。
浙公网安备 33010602011771号