使用 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 作为测试对象的识别语言,从而简化动态对象的识别问题。

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 对象的逻辑也要随时更新,就不得不修改大量的脚本代码,这大大增加了维护成本。

封装 RFT API 以实现 XPath 作为识别语言

接下来介绍如何实现这个想法。应用程序中的 GUI 对象是一个层次结构,一个对象有父对象也有子对象形成一颗树。把 GUI 对象看成 XML 中的元素,对象的类名就是 XML 中的元素标签,对象的属性就是 XML 中元素的属性。

我们以 eclipse 中”Add Bookmark”对话框 ( 图 1) 为例,它对应的 XML 表示如清单 1 所示。GUI 对象的父对象和子对象可以由 TestObject.getParent() 和 getChildren 得到。TestObject.getObjectClassName() 和 getProperty 可以获得对象的类名和属性。我们需要做的就是将 XPath 转换为对 RFT 底层 API 的调用。


图 1. 被测试的对话框
图 1. 被测试的对话框

清单 1. 对应的 XML 模型

<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 所示


清单 4. RFTXPath 的实现

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 程序
图 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 页面的源码
图 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);
}

 

实用技巧

另外,在实际应用中我们也总结了几点技巧可供读者参考。

  1. 为了更加方便的使用 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;
    }
}

 

  1. 给 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。


清单 7. 实现直接读取测试对象的属性值

/**
 * 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);
    }
}



清单 8. 直接读取测试对象的属性值示例

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 语法。

转载于:https://www.cnblogs.com/ibelieveKelly/archive/2013/04/27/3046939.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值