在 Rational Functional Tester 脚本中实现静态获取方法到动态获取方法的自动转换

背景

随着测试技术的发展,自动化测试越来越受到人们的关注。 Rational Functional Tester(RFT)就是基于应用程序图形界面(GUI)的自动化测试工具之一。 RFT 是基于 java 语言的测试工具,通过匹配对象属性来识别对象化的 UI 控件,进而操作这些 UI 元素完成一系列的事件和流程,实现自动化测试的功能。

一般情况下,在开始一个基于 RFT 的自动化测试项目的时候,都会选择静态的方法抓取对象,然后再对这些对象进行相应的操作,但是这种做法会给回归测试带来一些不便。当应用程序的 UI 有所改变时,静态的获取 UI 对象可能会变的比较困难。测试人员不得不重新抓取相应的对象。静态方法的这种不足正是我们提倡动态方法的意义所在。


静态获取方法

什么是静态获取方法?静态获取方法是通过 RFT 提供的测试对象录制(抓取)功能得到 UI 对象,并在测试脚本的运行中利用 UI 对象的所有属性来匹配 UI 对象的方法。静态获取的方法有简捷、快速的特点,但是当应用程序的 UI 发生变化后,它的识别能力就会减弱。这是因为 RFT 的这种静态获取对象的方法是通过匹配 UI 对象的所有属性来实现的,它可能造成这样的状况:UI 对象的位置、大小等次要参数如果发生重大变化,就可能导致匹配过程失败。


图 1.静态获取方法和测试对象地图 (Test Object Map)
静态获取方法和测试对象地图


动态获取方法

与静态方法不同,动态获取方法则通过匹配对象的关键属性(通常选择有意义的,不易变化的属性)来获取对象。这样对于软件的 回归测试来说,只要这个对象的这个关键属性没有变化,我们的脚本就不需要重新校正。这就使得 UI 对象的长宽、坐标等次要的属性信息不被用于匹配对象,当这些易变的、次要的属性变化的时候,脚本还可以照常工作。因此动态获取方法在伸缩性、健壮性等方 面,有着静态方法无法克服的优势,这将大大减少开发人员维护测试脚本的工作量。当测试下一个版本的时候,很少受到 GUI 界面变化的影响。

下面的代码示例了一个动态获取方法:

public SWTText getTxtFirstName_Org() {
GuiTestObject gto = (GuiTestObject)ObjectFactory.findTestObject("First name",
".priorLabel","org.eclipse.swt.widgets.Text",getTabDarOrderEditor());
if( gto == null ) return null;
return new SWTText(gto);
}

 

在上面的代码中,我们用 ObjectFactoryfindTestobject() 方法来实现动态获取对象。该方法的四个参数分别是:用来辨识对象的属性值、属性名称、对象的类型以及对象的父对象。该方法会把这四个参数作为获取对象的查找标准,与被测试 UI 中的对象进行匹配。在上面的代码中,我们试图匹配一个文本输入框,它的其中一个属性名称为“.priorLabel ”,也就是这个文本框的名称,属性值为“First name ”,classId 为“org.eclipse.swt.widgets.Text ”,说明它是一个文本框,最后的 getTabDarOrderEditor() 方法将返回一个对象,代表文本框的父对象。带着这些参数运行的 findTestobject() 方法将会匹配这样的对象:一个名称为 First name ,父对象是 getTabDarOrderEditor() 方法返回的对象的文本框。如果找到则返回一个 SWTText 对象,否则返回 null

注:本文的实现基于 IBM 的另一个工具,该工具提供了重新封装了 RFT 定义的各种对象,提供了一些诸如 ObjectFactory 类的工具,为动态获取对象的实现提供了很多帮助。读者可以从本文的参考资料部分得到该工具的介绍和源代码。


为什么静态获取方法要转换成动态获取方法

一般来说在新开发的测试脚本项目中,都会首先采用静态的方法获取对象。这是因为静态的方法简便、快捷。但随着不断有新的版 本的推出,静态获取方法需要被转化成动态获取方法的需求会越来越明显。这是由这两种获取方法的特点决定的。但是如果用手工去做相应的转化,不仅费时费力, 而且重复性的劳动会让测试人员身心俱疲。所以,能够自动完成此项功能的解决方案就显得尤为重要。


怎样将静态获取方法转换成动态获取方法

将静态脚本转化成动态脚本可以通过编程来实现。一般的过程是分析测试脚本对象,提取脚本中的静态对象,根据对象的 mapId ,取得其它一些动态方法需要的参数,比如识别类名和测试对象名,然后拼接字符串而生成动态获取方法。图 2 展示了转换静态方法和运行动态方法的过程:


图 2. 从静态方法到动态方法
从静态方法到动态方法


一个自动将静态脚本转化成动态脚本的工具

为了将静态脚本自动转化成动态脚本,我们开发了一个工具来完成这项工作。该工具主要实现三项功能:

  1. 根据脚本创建和更新配置文件,把用来查找对象的属性名及属性值抽取到了配置文件中。这样,即使以后某些对象的属性发生了变化,也可以通过修改配置文件来适应这些变化,而不必修改脚本本身。
  2. 将静态脚本转变为动态脚本。
  3. 从配置文件中查找和返回对象

下图是该工具的类图示例:


图 3. 工具的类图示例
工具的类图示例

这个工具工作的具体过程包括以下几个步骤:

1) 构建配置文件。首先获取测试脚本中所有的 UI 对象,遍历这些 UI 对象,取得 UI 对象的 mapId ,再据此得到其识别类名、Role 以及测试对象名,再据此得到 UI 对象的可供动态查找使用的属性名和属性值,如果无法得到合适的动态属性,则将这个对象的查找方法设置为静态查找,最后将这些信息保存到一个配置文件中。下图展示了该步骤的具体流程:


图 4. 持久化对象的属性到配置文件
持久化对象的属性到配置文件

值得注意的是,我们约定每个脚本文件(代表一个 RationalTestScript 对象)包含某个界面里的所有对象,为了便于统一处理,这个界面的最外层对象需要命名为“TopContainer ”。

下面是一些关键步骤的代码演示:

获取脚本中所有 UI 对象的名称。

IScriptDefinition sd = TS.getScriptDefinition();
Enumeration e = sd.getTestObjectNames();

 

创建一个 Document 对象来保存脚本中所有对象的信息,并将根元素 ( 或节点 ) 设为 TopContainer 。然后获取 TopContainer 对象的 mapID ,并默认的设置 TopContainer 的查找方式为静态,如果 TopContainer 对象能够动态获得,则设置查找方式为动态。接着获取必要的对象属性信息并保存到 Document 对象的元素(element)属性(attribute)中。最后再将该元素添加到 Document 对象中。下图展示了获取对象属性之一 className 的调用过程:


图 5. 获取对象属性的次序图
获取对象属性的次序图

查看大图

下面是关键步骤的代码演示:

// 新建一个 Document 对象,并将根元素 ( 或节点 ) 设为 TopContainer 。
DocumentBuilderFactory domfac = DocumentBuilderFactory.newInstance();
DocumentBuilder domBuilder = domfac.newDocumentBuilder();
Document config = domBuilder.newDocument();
Element root = config.createElement("TopContainer");
// 获取 TopContainer 的 mapID
String mapID = sd.getMapId("TopContainer");
// 默认 TopContainer 的查找方式为静态
root.setAttribute("isStatic", "true");
// 获取必要的信息并保存到元素属性中
root.setAttribute("class", TS.getMap().find(mapID).getClassName()
.toString());
root.setAttribute("role", sd.getRole("TopContainer").toString());
root.setAttribute("objClass", TS.getMap().find(mapID)
.getTestObjectClassName().toString());
root.setAttribute("propName", "mappedName");
root.setAttribute("propValue", "TopContainer");
root.setAttribute("parent", "self");
// 将元素添加到 Document 对象中
config.appendChild(root);

 

遍历所有对象,将需要的对象属性保存到 Document 对象中。该步的处理方式与上一步相似,区别仅在于需要遍历的处理所有非 TopContainer 的对象。

while (e.hasMoreElements()) {
String objName = e.nextElement().toString();
// 如果遍历到 TopContainer,略过
if (objName.equalsIgnoreCase("TopContainer"))
continue;
// 获取当前对象的 mapID
mapID = sd.getMapId(objName);
// 获取当前对象的类名
String className = TS.getMap().find(mapID).getClassName().toString();
// 获取当前对象的对象类名
String objClassName = TS.getMap().find(mapID)
.getTestObjectClassName().toString();
// 获取当前对象的 Role
String objRole = TS.getMap().find(mapID).getRole();
根据当前对象的类名、对象类名以及 Role 得到可作为动态查找的属性名
String propName = getDftPropForIdentify(className, objRole,
objClassName);
String isStatic = "false";
// 如果没找到匹配的属性,将此对象的查找方法设为静态
if (propName == null || propName == "") {
isStatic = "true";
Element node = config.createElement(objName);
node.setAttribute("isStatic", isStatic);
node.setAttribute("class", className);
node.setAttribute("role", objRole);
node.setAttribute("objClass", objClassName);
node.setAttribute("propName", "mappedName");
node.setAttribute("propValue", objName);
node.setAttribute("parent","TopContainer");
root.appendChild(node);
} else {
//得到可作为动态查找的所有属性
ArrayList propNames = getPropNameList(propName, "#");
String propNameToId = null;
String propValue = null;
boolean isFoundProp = false;
//遍历所有属性,得到当前对象具有的属性
for (int i = 0; i < propNames.size() && !isFoundProp; i++) {
propNameToId = (String) propNames.get(i);
try {
propValue = TS.getMap().find(mapID).getProperty(propNameToId).toString();
} catch (Exception ex) {
// 当前对象不具备此属性,不做处理,继续尝试其它属性
}
// 找到属性,退出循环
if (propValue != null && propValue != "")
isFoundProp = true;
}

// 如果属性值为空,将对象查找方法设为静态
if (propValue == null || propValue == "")
isStatic = "true";
// 将对象信息保存到 XML 对象
Element node = config.createElement(objName);
node.setAttribute("isStatic", isStatic);
node.setAttribute("class", className);
node.setAttribute("role", objRole);
node.setAttribute("objClass", objClassName);
node.setAttribute("propName", propNameToId);
node.setAttribute("propValue", propValue);
// 默认的父对象为 TopContainer
node.setAttribute("parent", "TopContainer");
root.appendChild(node);
}
}

 

持久化 Document 对象到 XML 文件中。当运行测试脚本时,将从此配置文件中读取对象属性去匹配 UI 上的对象。下面是一个配置文件的例子。


图 6. 配置文件的结构
配置文件的结构

2) 将静态脚本转变为动态脚本,这是实现动态获取 UI 对象的关键步骤。我们已经通过上面的步骤将脚本中所有对象的属性以 XML 格式保存在对象属性配置文件中,利用工具中的 getObj() 方法,就可以在脚本运行时,通过对比配置文件中的对象属性,定位到被测试 UI 中的对象。所以这一步的主要工作就是把原来脚本中获取对象的方法用工具提供的 getObj() 方法替换。为了避免手工替换脚本的巨大人力开销,我们提供了 updateDynamicObjMethod() 方法来实现脚本的自动转变。在这个方法中,我们将动态脚本分成三部分,头部、主体和尾部。其中,头部包括包声明、包引入、类声明以及读取配置文件中的信息;主体包含了所有的获取 UI 对象方法;尾部则在脚本文件共有的 testMain 方法中提供了一段代码,用以测试每一个获取对象的方法是否工作正常,下面分别展示了动态脚本三个部分的代码片断:

头部:

package appObjects.logon;

import ibm.widgets.WButton;
import ibm.widgets.WFrame;
import ibm.widgets.swt.SWTText;
import ibm.widgets.swt.SWTTree;

import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;

import resources.appObjects.logon.SC_DemoHelper;
import appObjects.ObjConfig;

import com.rational.test.ft.object.interfaces.TestObject;

public class SC_Demo extends SC_DemoHelper {

private static ObjConfig config = null;

public ObjConfig getObjConfig() {
return config;
}

public SC_Demo() throws Exception {
if (config == null) {
try {
String configFileFullName = this.getClass().getName();
String configFileName = configFileFullName.substring(
configFileFullName.lastIndexOf(".")
+ 1) + ".xml";
config = new ObjConfig(this.getClass().getResource(".."
+ File.separator + "config" + File.separator
+ configFileName).getPath());
}catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}

 

尾部:

public void testMain (Object[] args) throws Exception
{
Method[] methods = this.getClass().getDeclaredMethods();
ArrayList failedMethods = new ArrayList();
for(int i=0;i<methods.length;i++) {
System.out.println("processing:" + methods[i].getName());
TestObject to = null;
if(methods[i].getName().equalsIgnoreCase("getObjConfig") ||
methods[i].getName().equalsIgnoreCase("testMain")) continue;
try {
to = (TestObject)methods[i].invoke(this,null);
}catch(Exception e) {
e.printStackTrace();
failedMethods.add(methods[i].getName());
continue;
}
if (to == null) {
failedMethods.add(methods[i].getName());
}
}
if(failedMethods.size() == 0) {
System.out.println(
"Congratulations! Every method works well!"
);
}else {
System.out.println(
"The following methods can't work,please check them manually:"
);
for(int i=0;i<failedMethods.size();i++) {
System.out.println(failedMethods.get(i));
}
}
}
}

 

一般的,对于每一个动态脚本来说,头部和尾部的代码是相同的,不同的只是主体部分。因为每个脚本代表着不同的 UI 界面,所以其包含的具体对象也就不同,主体部分正是包含了对脚本中所有对象的获取方法。下面脚本主体部分的代码示例:

public WFrame getTopContainer() throws Exception {
return new WFrame(config.getObj(this,"TopContainer"));
}
public SWTText getTxtUserName_LogonForm() throws Exception {
return new SWTText(config.getObj(this,"txtUserName_LogonForm"));
}
public SWTText getTxtPwd_LogonForm() throws Exception {
return new SWTText(config.getObj(this,"txtPwd_LogonForm"));
}
public WButton getBtnLogon_logonForm() throws Exception {
return new WButton(config.getObj(this,"btnLogon_logonForm"));
}
public WButton getBtnCancel() throws Exception {
return new WButton(config.getObj(this,"btnCancel_logonForm"));
}

 

从上图主体部分的代码可以看到,getXXX() 方法会返回不同的对象类型,比如 Button , Label , 或者 Text ,而应该返回哪种对象类型,是由 classTestobjClassName 以及 Role 这些对象属性共同决定的,即这三种属性的一个特定组合匹配一个特定的返回类型。再配合某类对象可以用来动态查找的属性,我们就可以进行动态而精确的匹配。 当然,这类可以用作动态查找属性的属性有很多,因不同的对象类型而异,可以在对象匹配文件中自己指定——对象匹配文件以 XML 格式存储了区分各种对象类型需要的属性信息。下图展示了对象匹配文件中 Button 对象的属性匹配信息,我们根据 Button 对象的 classTestobjClassName 以及 Role 属性来辨别一个 Button 对象,再通过比较属性 text , toolTipText 或者 .priorLabel 的值来唯一的定位到某个 Button 对象:


图 7. 对象匹配文件的结构
对象匹配文件的结构

基于对象匹配文件,某类对象的返回类就可以由 getReturnClass 这个方法根据 classNamerole 以及 objClass 这三个参数,通过查找对象匹配文件而得到,如果没有找到匹配,则返回 ojbClassName

String wClassName = getReturnClass(className, role, objClassName);
if (wClassName == null || wClassName == "") {
wClassName = objClassName;
}

 

在动态脚本的主体中,所有对象的查找都是通过 getObj() 这个方法来实现的,该方法根据对象名从配置文件中取得用于查找此对象的属性及属性值。在此方法中,首先判断该对象能否被动态查找到。有些对象因为没有明显 的可以区别于其它对象的属性,只好采用传统方法,也就是静态方法。如果对象可以被动态获取,则从配置文件中取得属性值与实际的对象进行匹配,匹配成功则返 回该对象。下面是 getObj() 这个方法的代码示例:

// 根据对象名从配置文件中取得用于查找此对象的属性及属性值,置于 Map 中。
HashMap propMap = getObjProp(objName);

String isStatic = (String) propMap.get("isStatic");
// 返回静态对象
if (isStatic.equalsIgnoreCase("true")) {
String mappedName = objName;
TestObject to = new TestObject(TS.getMappedTestObject(objName));
if (to == null)
throw new Exception();
return to;
} else {
// 返回动态对象,属性值从配置文件中获得
String objClass = (String) propMap.get("class");
String propName = (String) propMap.get("propName");
String propValue = (String) propMap.get("propValue");
String parentName = (String) propMap.get("parent");
TestObject parent = getObj(TS, parentName);
ObjectFactory.SearchCriteria s = new ObjectFactory.SearchCriteria();
s.add(".class", objClass);
s.add(propName, propValue);

TestObject to = ObjectFactory.findTestObject(s, parent);
if (to == null)
throw new Exception();
return to;
}

 

3) 如果需要手工修改配置文件以及某些对象的获取方法。

需要注意的是,如果是修改了 TopContainer 的获取方法(默认为静态获取),需要将配置文件中 TopContainerisStatic 属性设为 “true

这样,使用这个工具将静态脚本改为动态脚本的过程可以简化为:

  1. 抓取静态对象到脚本中,并将其改名为有意义的名字。
  2. 运行工具,生成动态脚本。
  3. 根据需要修改某些对象的获取方法。

总结

在 RFT 的功能测试中,相较于静态方法,利用动态方法获取 UI 对象有着明显的优势。静态方法利用 UI 对象的所有属性来匹配对象,这使得一旦 UI 对象的属性发生些微变化,就会使匹配过程失败。静态方法的这一特点使得应用程序的任何变化都可能带来测试脚本的过时,测试人员不得不重新抓取对象,重新维 护脚本,这无疑增加了测试人员的投入,且使得脚本维护过程十分枯燥。而动态方法的出现正是为了克服这一缺憾,它利用 UI 对象的一个或多个属性来匹配对象,只要这些属性保持不变,匹配就不会失败。我们在选择匹配的属性时,也应该选择比较固定的,有意义属性,如对象名称,而不 宜选择经常变化的属性,比如对象的坐标、位置等。

所以,在 RFT 测试中,应该尽早实现动态方法,以减少以后的更多支出和繁琐。当然,动态化的过程和方法绝不只此一种,还需要不断的探索和完善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值