UML软件工程组织

使用 IBM Rational Functional Tester: 了解和使用 TestObject.find 方法
Mark Nowacki (mark_nowacki@us.ibm.com), 顾问软件工程师, IBM 软件集团, Lotus 软件
Lisa Nodwell (lnodwell@us.ibm.com), 顾问软件工程师, IBM 软件集团, Lotus 软件

本文内容包括:

TestObject.find 方法是 Functional Tester 的强大部分,可以使自动化的脚本更容易维护。本篇介绍性的文章向您展示了将 TestObject.find 引入到您的自动化框架中的简单方法。

 第 1 部分:TestObject.find 方法的基础

TestObject.find 方法是一个 Java? 方法,IBM? Rational? Functional Tester (RFT) 工具使用它在运行时,动态地在被测应用程序(application under test,AUT)中定位 TestObject。通过使用它,您可以避免不得不记录动作以向对象地图(对象地图(Object Map) ) 中添加 TestObject 对象。

被映射的对象使用被存储的、静态的、识别属性和对象层次来验证,在回放过程中脚本使用了正确的控件。虽然使用被记录的对象进行对象识别速度很快,但是更新属性却是费时的,特别是当您需要将对象的属性权值,或者文本属性变更为正则表达式(regular expression,Regex)值时。find 方法向您提供一种能够排除大多数来自于对象地图(对象地图(Object Map) ) 的被记录控件的选项。

TestObject.find 方法在 RFT 的 6.X 版本中更加健壮。现在,性能几乎与使用映射对象的性能相同。

使用 find 方法的好处

使用 find 取代对象地图(对象地图(Object Map) ) 的最好的原因中的一个是您可以很容易地变更存储在 Java 或属性文件中的控件识别属性。其优点是不必变更存储在对象地图(对象地图(Object Map) ) 中的识别属性,这样您就避免了使用对象地图(对象地图(Object Map) ) UI 做变更,或重新记录对象来进行更新的更加冗长且费时的方法。

了解及使用find

要了解 find 方法以及它到映射对象的关系,考虑可能会改变的应用程序中的控件,因而您需要更新对象地图(对象地图(Object Map) )。有些简单的控件方法,例如一个应用程序可能在版本更新过程中会变更其用户命令按钮的标签。例如,也许您的 AUT 中包含一个对话框,这个对话框中含有一个标签为Open的按钮。在以前的版本中,按钮的标签为OK。对于下一个版本,按钮的标签又变回了Open,但会有一个标签为Open with…的新按钮。

如果您将 OK按钮记录到对象地图(对象地图(Object Map) ) 中了,那么您将需要变更识别属性来适应标签变更,并且您将需要记录新的 Open with… 按钮。(对于此实例,我们将忽略对由于 UI 变更导致的应用程序中的对象层次的可能的变更。)

接下来,假设此对话框还包含了其他按钮,例如Cancel 和 Help。您可以看到按钮可以用标签来区分,即使未来它们在应用程序中以不同的顺序出现。因此,只要您能够用标签上的文字来区分这些按钮,那么您就不需要考虑索引(index)属性或其他任意属性。

利用 ClassicsJavaA 来探究 find方法

您还可以使用 RFT 来获取关于随后您也许需要寻找的控件的信息。对于此例,使用 RFT 提供的 ClassicsJavaA 示例应用程序,并依照以下步骤。

  1. 当您在定义按钮的属性时,创建一个临时的空的 Functional Test 脚本来保留对象地图(Object Map) 。
  2. 在空测试脚本里的菜单中,选择Script > Open Test Object Map 。
  3. 然后选择Applications > ClassicsJavaA 启动应用程序。
  4. 单击 Place Order 按钮。
  5. 单击 Member Logon 对话框中的 OK 按钮。
    现在应该出现 Place an Order 对话框。您可以使用该对话框来观察如何使用find 方法在对话框中定位按钮,以及为每个控件获得一个 TestObject。

图 1. ClassicsJavaA 应用程序中的 Place an Order 对话框


 您将会在下一个对话框中看到三个按钮:Related Items、Place Order,和 Cancel。观察对象地图(Object Map)中出现的这三个按钮,然后继续以下步骤:

  • 在 Private Test Object Map 中,从菜单中选择Test Object > Insert Object(s) 。
  • 将 Object Finder 控件拖到 Place an Order 对话框上,将其挪到标题栏上,以使整个对话框四周包围着一个红色方框,然后释放鼠标按钮。
  • 现在,在 Insert a GUI Object into the Object Map 对话框中,选择 Include all available objects on this window 并单击Finish。
  • 单击 Place Order 按钮。
  • 您现在的对象地图(Object Map)中应该有一个 javax.swing.JFrame 对象。选择 JFrame 控件,以便您可以看到对话框的 Recognition 属性。对于此控件,标签或文本是定义该控件的最重要的两个属性。第二重要的属性是控件的类,因为可能会显示出许多
 JFrame 对象,但是可能只有一个的标签为 Place an Order。继续以下步骤:
  • 展卡层次结构,找到按钮标签。
  • 选择 Cancel 按钮,以便您可以分析 RFT 用来在对象地图(Object Map)中描述按钮的 Recognition 属性。这里列出了四个属性,其中两个对定义正确的按钮是至关重要的:class 和 accessibleContext.accessibleName 属性(参见图 2)。

图 2. 使用 ClassicsJavaA 应用程序的对象地图(Object Map)的实例

  • 找到正确的按钮。许多对话框中包含一个 Cancel 按钮,所以您首先需要找到包含正确的 Cancel 按钮的对话框 TestObject。如果您首先找到了正确的对话框,那么找到对话框中正确的按钮就更容易了。
  • 回到您的空测试脚本中。因为对话框是高级对象,所以您可以通过定义 RootTestObject 来开始 TestObject 搜索。当您做完这些后,
  • 就可以使用 find 方法来定位对话框 TestObject:
    RootTestObject root = getRootTestObject();
  • 您已准备好使用 find 方法来定位 Place an Order 对话框,利用窗口的类信息。

注意:确保打印出您找到的所有 TestObject 的属性,这样您可以检查它们。

您的命令应类似以下内容:

// get all test objects of type JFrame (dialog)
TestObject[] to = root.
find(atDescendant("class", "javax.swing.JFrame"));

// print dialog test object properties to console
for (int i = 0; i < to.length; i++)
{
System.out.println (to[i].getProperties());
}

结果显示应该类似以下内容(除去了大部分属性,为了节省空间):

{height=600, displayable=true, undecorated=false, ...,
class=javax.swing.JFrame, title=Place an Order, …}

接下来,使用 find 方法代替上面的代码,利用对话框中该窗口的标签文本来定位 Place an Order 对话框:

// get all test objects that have the title "Place an Order"
TestObject[] to = root.
find(atDescendant("title", "Place an Order"));

// print test object properties to console
for (int i = 0; i < to.length; i++)
{
System.out.println(to[i].getProperties());
}

在两种情况下,您会发现只有一个对象和被显示的属性是相同的。这是理想的情况。要提高每种情况下该结果的可能性,您可以将两个属性组合成一个 find 调用:

// get all test objects of type JFrame (dialog)
// AND have the caption "Place an Order"
TestObject[] to = root.
find(atDescendant(("class", "javax.swing.JFrame",
"title", "Place an Order"));

既然您已经找到了包含带有您搜索的标签的按钮的控件,那么您就可以使用 find 来定位正确的按钮了:

// capture the correct dialog as a test object
TestObject toPlaceAnOrderDialog = to[0];

// reuse the test object array, finding all buttons on the
// "Place an Order" dialog that have the caption "Cancel"
to = toPlaceAnOrderDialog.
find(atDescendant("class", "javax.swing.JButton",
"text", "Cancel"));

// verify that only one button was found
System.out.println(to.length);

// capture the correct button as a GuiTestObject
GuiTestObject cancelButton = new GuiTestObject(to[0]);

您可以从任何 TestObject 来调用 find方法。这依赖于您所选的对象,搜索只限于您所选择的对象下面层级的对象。

在上面的实例中,代码使用 atDescendant 来定位按钮。然而,您可以有许多方式来使用 find:

  • atChild 搜索 TestObject 所有直接的子对象。
  • atDescendant 寻找 TestObject 的所有子对象。
  • atList 让您指定一个 atChild、atDescendant,和 atProperty 对象的列表,这样您可以减小搜索范围。

第 2 部分:使用 TestObject.find 方法的实际应用程序的实例

试验各种属性将帮助您确定哪个属性在寻找应用程序中的对象时工作的最好。您一旦了解了 find 方法如何帮助您动态定义控件,您就可以开始编写 getter 方法了,这样您就可以不依赖被记录的对象地图(Object Map)来定位对象了。

将控件定义为对话框的一部分

例如,对于 Place an Order 对话框,您可能决定将控件定义为对话框的一部分。在这种情况下,您应首先找到该对话框,然后,到对话框中找您希望修改的控件。对于该情境,您的代码设计应该如下所示:

public class PlaceAnOrder
{
public static GuiTestObject getDialog()
{
RootTestObject root = getRootTestObject();
TestObject[] to = root.
find(atDescendant("title", "Place an Order"));
return new GuiTestObject (to[0]);
}

public static GuiTestObject getButtonCancel()
{
TestObject[] to = getDialog()
.find(atDescendant("class", "javax.swing.JButton",
"text", "Cancel"));
return new GuiTestObject(to[0]);
}
}

这里是一个说明如何在脚本中使用这些方法的实例:

public void testMain(Object[] args)
{
// Find the Place an Order dialog
GuiTestObject dialogPlaceAnOrder = PlaceAnOrder.getDialog();
GuiTestObject cancelOrder = PlaceAnOrder.getButtonCancel();
cancelOrder.click ();
}

为找到通用的对象定义方法

要找到更多通用的按钮标签,例如,Cancel,您可能需要开发更多能够找到存在于您的应用程序中的任意按钮的通用方法。例如,您可以使用getButton("Cancel", placeAnOrderDialog),在此,来源是父窗口。对于第二个情景,您的设计应该类似于此实例:

public class ClassicsJavaUI
{
public static GuiTestObject getButton(String buttonName,
TestObject parent)
{
TestObject[] to = parent.find(SubitemFactory.atDescendant
("class", "javax.swing.JButton", "text", buttonName));
return(new GuiTestObject(to[0]));
}

public static GuiTestObject getButton(String buttonName)
{
RootTestObject root = RationalTestScript.getRootTestObject();
TestObject[] to = root.find(SubitemFactory.atDescendant
("class", "avax.swing.JButton", "text", buttonName));
return (new GuiTestObject(to[0]));
}

public static GuiTestObject getDialog(String dialogName)
{
RootTestObject root = RationalTestScript.getRootTestObject();
TestObject[] to = root.find(SubitemFactory.atDescendant
("class", "javax.swing.JFrame", "title", dialogName));
System.out.println (to.length);
return (new GuiTestObject (to[0]));
}
}

public class PlaceOrderWindow
{
public static GuiTestObject getDialog()
{
return ClassicsJavaUI.getDialog("Place an Order");
}
public static GuiTestObject getButtonPlaceOrder()
{
return ClassicsJavaUI.getButton("Place an Order", getDialog());
}
}

这里是一个您可以如何在脚本中使用这些方法的实例,或者更适当地说,利用定义了 Place An Order 对话框的类:

public void testMain(Object[] args)
{
GuiTestObject dialog = PlaceOrderWindow.getDialog();
GuiTestObject buttonPlaceOrder =
PlaceOrderWindow.getButtonPlaceOrder();
buttonPlaceOrder.click ();
}

您测试的应用程序将确定您为寻找控件如何定义方法。下面部分是一个实际的例子,说明了系统是如何围绕 find 方法而设计的。

在基于 Eclipse 的应用程序中实际使用 RFT find 方法

在测试基于 Eclipse 的应用程序中使用 find 方法的这个实例仅局限于 AUT 的一个部分,作为得出对象 getter 方法的设计的一个途径。本应用程序中的对象具有可识别的模式。标准的 SWT 对象是主要被使用的,但还有应用程序开发团队提供的一些专门的定制类。

测试开发人员创建一个通用的方法来获取一个特定类的所有对象,或者获取一个具体父对象下给定索引号的一个对象。要使得自动化在应用程序的局部版本上工作,测试开发人员决定使用对象索引作为寻找对象的关键属性,而不是文本。下面介绍的类将用于做这件事。

注意:这些类中的所有方法都是静态的,所以测试开发人员不需要将这个类实例化为一个对象就可以使用这些方法。可以通过使用以下格式来调用它们:<class name>. <method name> (<parameters>)。

FindAppObjects.java 类可以让您使用两个不同的方法:getAllObjects 和 getObjectAtIndex 。这些方法通常不直接使用,但它们是其他方法的基础。然而,您可以直接使用它们来确定通过哪些索引号找到哪些对象。

/**
* Given a parent object, find all descendant test objects with a
* given class.
* @param parent TestObject
* @param className String
* @return Array of TestObject or null if no objects of type
* className were found.
*/
public static TestObject[] getAllObjects(TestObject parent,
String className)

getAllObjects 将 TestObject 类型的父对象和 String 类型的字符串为输入,并返回包含了父对象的具体类型的所有子对象的 TestObject 的数组。此实例返回 RootTestObject 的所有子对话框:

RootTestObject root = getRootTestObject ();
TestObject[] dialogs =
FindAppObjects.getAllObjects(root, "javax.swing.JFrame");

选择正确的父 TestObject 对象是重要的,以便您可以从结果中只获得您正在寻找的对象。作为一个实例,假设下面的部分表示 AUT 中的一个对象层次:

application
composite
group0
button0
button1
group1
button0
button1

Calling getAllObjects (application, "button") 将返回一个含有四个按钮的数组。然而,如果不循环扫描该数组,并打印出每个索引号和 button[index].getProperty("text") 字符串的话,就不能很快确定哪些索引号匹配哪些按钮。

要取代这种麻烦的过程,进行一个以上的分层调用更有意义:

// get all groups in the application
TestObject[] groups = FindAppObjects.getAllObjects
(application, "group");

// limit search to only buttons in the first group found
TestObject[] buttons = FindAppObjects.getAllObjects
(groups[0], "button");

// click the first button found under the group
new Button(to[0]).click();

getAllObjects (group0, "button") 调用只返回两个按钮,它们很可能处于正确的顺序(button0,然后 button1),如屏幕上显示的。如果整个应用程序或对话框中只有一些按钮,那么获得所有的按钮并且计算出索引可能会更容易。

/**
* Given a parent object, find the descendent test object
* with a given class, at a given one-based index.
*
* @param parent TestObject
* @param className String
* @param index int, zero-based index
* @return TestObject or null if index is greater than
* the total number of objects found.
*/
public static TestObject getObjectAtIndex
(TestObject parent, String className, int index)

getObjectAtIndex 精炼了 getAllObjects 的使用。它获取一个父的 TestObject 对象、一个 String 类的字符串对象,和一个以零为基数的整数,然后返回一个单个的 TestObject 对象。例如,此代码返回指定(父)分组下的第一个按钮:

TestObject to = getObjectAtIndex(group1, BUTTON, 0);

这些方法都返回一个 TestObject 对象,并且那些可能不是您需要找的具体类型的对象。上面的两个方法都返回一般的 TestObject 对象。常常,您希望使用具体类型的对象。当使用这些方法时,确保在您将对象用于代码中之前,显式地将所返回的 TestObject 对象转换为正确的类型。

FindBasicAppObjects.java 类提供进一步的细化和辅助。它是 FindAppObjects.java 的子类,并且使用了 getObjectAtIndex 方法。它包含返回 TestObject 父对象的索引号位置的子对象的 getter 方法。这时产品对象已经被转换成为了正确的类,所以您不需要自己再次的转换它。此实例返回标准的 Eclipse SWT widget 类的一个子集,例如 org.eclipse.swt.widgets.Button:

/**
* Find the index-specified org.eclipse.swt.widgets.Button
* child control under the specified parent.
*
* @param parent TestObject
* @param index int, zero-based
* @return WButton
*/
public static WButton getButton(TestObject parent, int index)

getDialog 是 FindBasicAppObjects.java 中唯一一个不需要父亲对象的方法,因为它假设父对象是 AUT。为了能够识别正确的对话框,即使该对话框的标题是易变的,getDialog 将正则表达式(Regex)对象作为参数,如下所示:

/**
* Find a org.eclipse.swt.widgets.Shell control
* with a caption specified by the given
* Regular Expression (Regex).
*
* @param dialogCaption Regex object containing dialog's
* expected caption
* @return WFrame
*/
public static WFrame getDialog (Regex dialogCaption)

比较 Place an Order 对话框的代码(在此部分的开头)和在下面实例中使用FindAppObjects 和 FindBasicAppObjects 的同样的行为。假设 Place an Order 对话框是 WFrame 类型的 Eclipse 对话框,并且按钮是 WButton 类型的。

public class PlaceAnOrder extends RationalTestScript
{
private final int CANCEL = 2;
private final Regex DIALOG_CAPTION = new Regex
("^Place an Order$");

public WFrame getDialog()
{
return FindBasicAppObjects.getDialog(DIALOG_CAPTION);
}

public WButton getButtonCancel()
{
return FindBasicAppObjects.getButton(getDialog(),CANCEL);
}

public void testMain(Object[] args)
{
getButtonCancel().click();
}
}

前面的代码是以很容易维护的方式编写的,代码中每个部分都分隔为容易理解的一块。对于喜欢更加紧凑布局的编码人员,该代码还可以以此方式书写:

public class PlaceAnOrder extends RationalTestScript
{
public void testMain(Object[] args)
{
FindBasicAppObjects.
getButton (FindBasicAppObjects.
getDialog (DIALOG_CAPTION), CANCEL).click();
}
}

总是编写单元测试

除了基于 find的 getter 方法以外,前面提到的 FindBasicAppObjects.java 类还包含每个已定义的对象类型的基本的单元测试方法。每个单元测试方法获取一个具体类的对象和一个字符串(字符串应该是您在测试的 getter 方法的名称)。该名称被发送到控制台,以跟踪通过或是失败的结果。

单元测试方法首先验证所提供的对象不是空的,然后对该对象执行一个或多个非常基础的操作。例如,WButton 对象的单元测试方法打印出该对象的文本属性(标签)。您可以看到 getter 方法是否返回正确的按钮。

推荐使用单元测试方法来测试每个 getter 方法。单元测试常常被视为多余的工作,但在对应用程序的用户界面(UI)做任何变更之后,运行单元测试将验证方法能继续找到 AUT 中正确的控件,并将确认 getter 方法中的任何代码变更能正确工作。运行单元测试确保测试用例能继续找到并返回正确的对象。

/**
* Unit test for button controls.
*
* @param b
* @param methodName Name of method under test
*/
public static void verifyButton(WButton b, String methodName)

调用单元测试方法的实例:

FindBasicAppObjects.
verifyButton(getButtonCancel(),"getButtonCancel");

局限性和建议

总是在使用控件之间找到它,不要依赖于代码中存储的 TestObject。您找到的对象可能在您第一次找到它时和您下次需要使用它时之间变更了(或者删除并重新生成了)。每次您处理一个对象时,都需要调用find 方法来确保您得到正确的对象。

如果您使用 find 方法,您不能记录脚本。因为您的 TestObject 不是在对象地图(Object Map)中定义的,在记录中,记录器不能挑出正确的对象名称。您仍旧可以对您的脚本所需的基本结构代码进行记录,但您需要用基于 find 的 getter 方法来取代所记录的被映射控件的 getter 方法。

许多对象可能会显示出一样的结果。当您调用 find 方法时,可能会存在一个以上的 OK 按钮。确保通过使用不同的属性,例如 OK 按钮所属于的对话框,来获得正确的按钮。。将 TestObject 的 getProperties 方法的结果打印到控制台上,以观察哪个属性可以帮助您在类似的对象中区分出您所找的对象。


版权所有:UML软件工程组织