转自:http://www.ibm.com/developerworks/lotus/library/rft-api/index.html
The Rational GUI automation tool has a wonderful recorder feature that records a user's activities and automatically generates code that simulates these activities. You can then immediately play back this code with no further revision necessary to perform simple simulation testing. However, large and complex applications, such as IBM WebSphere Portal, IBM Workplace Collaboration Services, or IBM Workplace Client Technology, often require more sophisticated testing than unmodified record and playback can provide. For our own internal testing of IBM Workplace products, we used Rational Functional Tester(formerly Rational XDE Tester) to manually write code for our simulation testing. In the process, we've learned several ways to make writing code for these products easier and faster.
Rational Functional Tester is based on Java, which makes it very easy to extend its built-in functionality. This allowed us to create wrapper classes in Java that encapsulate calls to the Rational Functional Tester API to perform common operations on controls. These classes greatly improved our productivity when manually writing and maintaining the automation code we use to test our products. Over the past year, these classes have been rolled out to many other software teams across IBM.
In this article, we show you how we created these wrapper classes, which we call widget classes. This article assumes that you have experience writing test scripts with Java. We lead you through the coding of widget classes for testing text fields, list boxes, and other standard controls; for searching for dynamically-generated controls; and for extending the classes to handle custom Java controls. If you plan to test complex applications, such as applications integrated with WebSphere Portal or IBM Workplace or Eclipse-based applications to use with Workplace Client Technology, we think you will see great value in these classes, and we hope this article will inspire you to create some of your own.
Widget class: TextField
Text fields provide a good illustration of the difficulties we faced in manually coding Rational Functional Tester test scripts. When you record the action of setting the text of a text field, you get code that looks something like this:
Text_name().click(atPoint(35,10)); Browser_htmlBrowser(document_C(),DEFAULT_FLAGS).inputKeys ("{ExtHome}+{ExtEnd}{ExtDelete}"); Browser_htmlBrowser(document_C(),DEFAULT_FLAGS).inputKeys("test");
This is very wordy code. It takes three lines to accomplish one task: setting the text. But it is understandable behavior from the recorder. Because the recorder doesn't know that you want to set the text (rather than, for example, clicking the text field or typing some text without erasing it first), the recorder can't replace one method call for all three lines.
However, if you are writing code manually instead of recording, it becomes a hassle to type in all three of these lines of code. For manual coding, what you really need is a setText() method so that you can type one line of code instead of three. To accomplish this, you can wrap the three lines of code produced by the recorder into a separate method in your test script.
As a first approximation, you could create a setText() method that looks like this:
void setText(String s) { Text_name().click(); Browser_htmlBrowser().inputKeys("{ExtHome}+{ExtEnd}{ExtDelete}"); Browser_htmlBrowser().inputKeys(s); }
Of course, this won't do because it will only work for one particular text field in the object map, namely Text_name. It would be better to generalize it for any text field. One way to do this is to pass the object into the method as a parameter like so:
void setText(String s, GuiTestObject gto) { gto.click(); Browser_htmlBrowser().inputKeys("{ExtHome}+{ExtEnd}{ExtDelete}"); Browser_htmlBrowser().inputKeys(s); }
This will do so long as all you are worried about is the current test script that you are working with. But what you really want for the long run is a method that is accessible from all the test scripts that you write. One way to enable this is to put the method into a Super Helper, and then to inherit that Super Helper into all your test scripts. (For more on Super Helpers, see the developerWorks: Rational article, "Creating a super helper class in Rational XDE Tester.") After you create the methods that you need for all the different kinds of controls (text fields, list boxes, and so on), it makes for a very large and unwieldy Super Helper. A better idea is to create separate Java classes corresponding to each kind of control and to put the methods for manipulating each control into the corresponding class.
To create a TextField widget class, create a new Java class in Eclipse by invoking the wizard in the Rational Functional Tester development environment.
- In Rational Functional Tester IDE, choose File - New - Other. (Do not choose File - New - XDE Tester Script.)
- Select Java and Class and click the Next button.
- Enter the name of the class TextField into the Name field. Leave the rest of the fields to their default, and click Finish.
After you create the new Java class, you can put the following revised setText() method into that class:
public class TextField { public static void setText(String s, GuiTestObject gto) { gto.click(); TopLevelTestObject app = (TopLevelTestObject) gto.getTopParent(); app.inputKeys("{ExtHome}+{ExtEnd}{ExtDelete}"); app.inputKeys(s); app.unregister(); } }
Now you can call this method from your test script with the simple line:
TextField.setText("test", Text_name());
As you can see, this is much easier to code manually than entering each operation in the same way that the recorder creates code.
At this point, you want to add other methods to the TextField class. At the very least, you should add another method to the class that gets the text of a text field as follows:
public static String getText(GuiTestObject gto) {
return gto.getProperty(".value").toString();
}
Methods that get information from controls are very handy to wrap in this way. It is very difficult to remember the property values that you need to get information directly from the GuiTestObject, but it is very easy to remember a method like getText(), so you will find that wrapping getProperty() calls like this is extremely useful.
However, you don't want to wrap every method that Rational Functional Tester provides for manipulating text fields. If someone wants to drag across a text field for some reason, or hover over it, you certainly want to allow them to do that, but it would be mind-numbingly dull to write a wrapper method for every method in GuiTestObject, each one calling the corresponding method that Rational Functional Tester already provides. Instead, you can inherit Rational Functional Tester's GuiTestObject into your class. Then the user of this class can use any of the existing methods in GuiTestObject, and you don't have to rewrite all of them.
If you inherit GuiTestObject, however, you need to write constructors. After you do that, change your existing methods to no longer remain static. Instead, you can pass in the GuiTestObject to the constructor once, and then use the methods to set and get the text as instance methods instead of class methods. (Also, static methods can cause problems if people extend your classes because they can't be overridden properly.) This makes it a little bit less intuitive to call the method. Instead of:
TextField.setText("test", Text_name());
you have to use:
TextBox tb = new TextBox(Text_name());
tb.setText("test");
Or you could combine this into one statement if you do not use this particular text field more than once in your script:
new TextBox(Text_name()).setText("test");
So this is a bit less intuitive (especially to those unfamiliar with Java), but it greatly improves the overall design.
When you finish inheriting GuiTestObject, your final TextField class looks like this:
public class TextField extends GuiTestObject { public TextField(GuiTestObject textfield) { super(textfield); } public void setText(String s) { this.click(); TopLevelTestObject app = (TopLevelTestObject) gto.getTopParent(); app.inputKeys("{ExtHome}+{ExtEnd}{ExtDelete}"); app.inputKeys(s); app.unregister(); } public String getText() { return this.getProperty(".value").toString(); } }
In the TextField class we use at IBM, we have a lot more methods, including one to clear the text and one to enter escape keys both of which are used by our setText() method. In this article, we kept this simple to convey the gist of the strategy. You can add additional methods as you see fit.
Widget class: Listbox
List boxes present even more challenges to code manually in Rational Functional Tester than text fields. There are numerous operations you often need to perform on list boxes, such as selecting an item by name or integer, getting the name or index of the item selected, getting the number of possible selections, and getting a list of all the names of the selections. But these actions require typing several lines of hard-to-remember code in Rational Functional Tester.
For example, every time we wanted to select an item in a list box, we found it rather tedious to type this:
List_category().click(); //click once to expose the drop down list
List_category().click(atText("Computer Related")); //then select the text
rather than this:
new ListBox(List_category()).select("Computer Related"));
In addition, if we wanted to make sure the item existed before we selected it, using Rational Functional Tester's API, we had to do something like this:
ITestDataList dataList = (ITestDataList)this.getTestData("list"); ITestDataElementList elementList = (ITestDataElementList)dataList.getElements(); for(int i = 0; i<elementList.getLength(); i++) { if (elementList.getElement(i).getElement().toString(). equals("Computer Related")) { List_category().click(); List_category().click(atText("Computer Related")); } }
Much easier would be to use a widget class that wraps this functionality into easy-to-remember methods, like this:
ListBox lb = new ListBox(List_category()); if (lb.doesItemExist("Computer Related") { lb.select("Computer Related")); }
To accomplish this, create a ListBox widget class similar to the one created earlier for TextField, and then add select() and find() methods to it. The select() method is pretty straightforward, but the find() method, as we implemented it, makes use of other methods that we have exposed in the class. We show you our entire ListBox widget class (cleaned up for presentation purposes, of course). This gives you an idea of the many methods we've found useful for manipulating list boxes. As you'll see, there are a lot of them, so this class should show you where you can get some real bang for your buck from creating widget classes. To see the ListBox widget class, view this sidefile.
Widget classes: other common controls
You might think that there's not much reason to create widget classes for controls other than text fields and list boxes. The other common controls, such as links, buttons, checkboxes, and radio buttons, are really not that difficult to work with. Most of the time, a click() will do. Other than being consistent with the widgets already created, why create separate classes for these other controls?
The answer is twofold. First, there are properties you often need to get from certain controls. As we discussed earlier, it is much more intuitive to call a getText() method than to call getProperty(".value").toString(). The most obvious controls where this is useful, in addition to text fields and list boxes, are checkboxes and radio buttons.
For example, not only do you often want to select a checkbox, but frequently you also want to know whether or not the checkbox is selected. We supplied an isChecked() method in our CheckBox widget class that performs this operation. We found it easier to remember how to write this:
new CheckBox(CheckBox_1()).isChecked();
than this:
CheckBox_1().getState().isSelected();
Furthermore, this allowed us to create a check() and uncheck() method which queried the state of the object before performing the operation. Similarly, we found it much easier to remember how to write this:
new CheckBox(CheckBox_1()).check();
than this:
CheckBox_1().clickToState(State.selected());
every time we wanted to select a checkbox.
Second, and more important, we found that we often needed to find certain types of controls dynamically either because the controls were generated at runtime or because their location changed from build to build. We found that this capability was used frequently with links, static text, images, and buttons, so we added special methods in these widgets to locate them dynamically, such as this one which finds a link based on its text property:
public static Link findHtmlLinkFromTextProp(Regex linkText, TestObject parent) { TestObject to = ObjectFactory.findTestObject( linkText, ".text", "Html.A", parent); //Make sure the object exists if (to == null) throw new ObjectNotFoundException("Link with text '" + linkText + "' not found"); return new Link(to); }
This method calls another method in a class called ObjectFactory that does all our dynamic searching. We have found dynamic searching to be so useful, it is worthwhile to examine our ObjectFactory class in more detail.
ObjectFactory: searching for controls
In some Web applications, controls are often dynamically generated at runtime. For example, a standard Web-based email application such as IBM Workplace Messaging lists messages in the Inbox as links.
Figure 1. IBM Workplace Messaging
Each new message that comes into the Inbox creates a brand-new dynamically generated link. This presents a problem for Rational Functional Tester's object model. If you map the link as it appears in Figure 1, when you run the test again, the order of the links may change as more mail gets sent to the Inbox. As the order changes, so will the recognition properties of the link, which may cause the automation to fail. What you need instead is a way to recognize the link at runtime, in other words, a way to locate the link dynamically. This is what our ObjectFactory class does.
The easiest way to focus on the issue is to consider a case in which you want to randomly generate unique names for links in a dynamic Web application. In our WebSphere Portal automation, we do just that. WebSphere Portal presents the following Web page form when creating a new Collaborative Space or Place:
Figure 2. WebSphere Portal page
We need to create a new place for every test we run, so we enter a randomly generated name to uniquely identify each place. After the Create button is clicked, a new, uniquely named place is generated and the link for that place now appears on the list of places as seen in Figure 3.
Figure 3. Link to new place
However, if you can't dynamically access this newly generated, uniquely named link, you can't continue the automated test. Because you don't know the name of the place before runtime, you can't add the resulting link to the object map when you are recording your test case. Instead, you need to dynamically access the link at runtime.
The ObjectFactory class addresses this issue with dynamic object recognition methods. These methods help the automation developer easily find and act on dynamically generated GUI objects--objects that are generated at runtime. Using the findTestObject() method in the ObjectFactory class, the automation developer can simply access the dynamically created place link created earlier with the following call:
ObjectFactory.findTestObject("AAAAPlace_721134315", ".text", "Html.A", Browser_Page());
This method searches the entire page content for an object of Link class with the text AAAAPlace_721134315 and returns the object to be acted upon.
The findTestObject() method in the ObjectFactory class calls a recursive method named findTestObjectRF(), which is shown in the following code. This method accepts four arguments: a property, a property value, a class, and a parent test object. And with that information, it searches for an object that has the given property value among the descendents of the parent TestObject.
public TestObject findTestObjectRF(String sProperty, String sValue, String sClassID, TestObject t) { //check if object was found. If yes return it. if (gbKill) return t; else gbKill = false; //declare array of TestObjects TestObject[] objList; //get top level children if (sClassID.equals(gsTextRef)) { objList = t.getChildren(); } else { objList = t.getMappableChildren(); } //Find dynamic web object for (int x = 0; x < objList.length; x++) { if (gbKill) return t; //try to find specified object try { //check for specified class if (objList[x].getProperty(gsClassProp).toString() != null) { //check for specified property if (objList[x].getProperty(gsClassProp).toString() .indexOf(sClassID) != -1) { //check for specified property value if (objList[x].getProperty(sProperty) .toString().indexOf(sValue) != -1) { gbKill = true; //if test object is found set to true gtoTestObj = objList[x]; //set global test object holder to appropriate test object return gtoTestObj; //return found test object } } } } catch (Exception e) { //continue; } //get child objects if (sClassID.equals(gsTextRef)) { if (objList[x].getChildren().length > 0) { findTestObjectRF(sProperty, sValue, sClassID, objList[x] ); } } else { if (objList[x].getMappableChildren().length > 0) { findTestObjectRF(sProperty, sValue, sClassID, objList[x] ); } } } //end loop return gtoTestObj; }
This method searches within the given parent test object, usually a browser page, for an object of a specified class, for example, Link, TextField, Listbox, and so on. After it finds an object of the specified class, it checks to see whether or not the object's specified property value is equal to the search value. If the property value matches the search value, then the method has successfully found the desired test object and returns it. In this way, you can dynamically search for any object that has any given property value in your application.
NOTE: As we go to press, the just-released version of Rational Functional Tester 6.1 has a dynamic object search capability built into a TestObject method called find(). We will be substituting a call to this find() method for our recursive function described previously when we get this new version, and so should you. The main point to be taken away from this is how useful such a find() method can be.
Another situation in which the dynamic search capability is useful is when your application changes drastically with each new build. Often, we find that each new build of our application under test changes our object recognition properties enough that our automation breaks. We have daily builds, so this means our object maps need to be updated frequently. One of the ways we have limited the need to constantly manage and update object maps is to make use of the dynamic object search capability of the ObjectFactory class. Instead of recording (mapping) all of the objects in our constantly changing application, we map only the most essential objects and use the findTestObject() dynamic search method to find the rest.
Because this is a problem for any type of control, not just links, we added a constructor to all our widgets, which finds the control dynamically, and then constructs the widget. For example, our TextField widget has a constructor that looks like this:
public TextField(String sTextField, String sProperty, String sClass, TestObject parent) { super(ObjectFactory.findTestObject(sTextField, sProperty, sClass, parent)); }
We have a similar constructor in all our other widget classes so that we can find and construct any type of widget dynamically.
Finally, we should also mention that we created a StaticText widget class. In Rational Functional Tester, you cannot map HTML static text. However, it is often very useful to find static text on a Web page, for example, to verify the text appearing in an HTML table or document. For this reason, we created a StaticText widget that can be constructed dynamically and also has static dynamic find() methods similar to those we created for our Link widget.
As you can see, the dynamic search capability built into the ObjectFactory class can be extremely useful for manipulating objects generated at runtime or to provide some stability for constantly changing GUI object maps. We have built many other variations on our main dynamic search method into our ObjectFactory class, variations that search based on regular expressions, search for the nth occurrence of a control with a particular property, and so on. We call many of these methods in our widget classes, making our widget classes even more powerful.
Extending the widgets
So far, we've focused mostly on the widget classes from the perspective of HTML testing. However, these same widget classes with minor modifications will also work to test Java applications. When modifying them for Java applications, change the values you query from methods that use getProperty() because these values are often different, even for the same types of controls. There are other minor differences, but they are pretty easy to code around.
To make our widget classes applicable to testing both HTML and Java applications, we created the following method in our ObjectFactory class:
public static boolean isHTML(TestObject to) { String sClass = to.getObjectClassName(); return sClass.indexOf("Html.") != -1; }
When a method has to call different Rational Functional Tester API code to perform the same action on Java and HTML, we used this method to test whether or not the current object we were working with was Java or HTML and called the appropriate code depending on the answer. In this way, we enabled our widget classes to be used for testing either Java or HTML applications.
You might find that you have Java controls that extend the functionality of the standard controls discussed previously in this article. When this is the case, you can create a new widget class that inherits the functionality of the standard widgets upon which the control is based, and then add the extra functionality that the new control requires. For example, in Java, there are true comboboxes that combine the functionality of list boxes and text fields. To deal with this, we created a ComboBox widget class that inherited ListBox and added a setText() method.
Furthermore, you may find that your application uses custom controls that supply much more extensive functionality. That is, your developers may create original controls in-house. Sometimes you can manipulate such controls using the basic methods provided by Rational Functional Tester's GuiTestObject (for example, click(), getChildren(), and so on). In such cases, you can create a wrapper method that calls these GuiTestObject methods.
Often, however, to manipulate custom controls effectively, you need information about the contents of a Java control that is only accessible through the public methods of that control. Fortunately, Rational Functional Tester provides an interface through which you can call any public method of a Java control, namely the invoke() method of TestObject. When you face such a situation, wrap the custom control in its own widget class, inheriting other standard widgets when appropriate, and then wrap the invoke() call in a method of this class.
To take a simple example, our developers created a Java applet that presented a rich text field to the user. In this field, the user could not only type in text, but he or she could make the text appear bold, italic, or underlined and use different colors and fonts and so on. The control looks like this:
Figure 4. Custom rich text control
When testing this control, we not only wanted to make the text inside the control italic or bold (which could be accomplished by clicking one of the buttons), but we also wanted to query whether or not the control was currently set to make the text italic, bold, or whatever. To do this, we needed to access the Java methods inside the control itself. We asked our developers to expose the methods we needed to call by making them public, and then we could access them using Rational Functional Tester's invoke() method like so:
public boolean isBold() { return ((Boolean)getBoldButton().invoke("getSelected")) .booleanValue(); }
Of course, we have many other methods in this class that use similar calls to the public methods inside the control, and we have matching widget classes for all the other Java applet controls in this application. But this simple example shows how to wrap an invoke() call inside a method in a widget class to make your scripting code simpler to write and maintain.
A colleague of ours used this technique to create widget classes for testing SWT (Standard Widget Toolkit controls, which are a particular set of Java GUI controls produced by IBM and used in Eclipse applications. This technique of wrapping invoke() calls inside the methods of a widget class enabled us to much more easily code automation that manipulated these SWT controls. Because the SWT controls are used extensively throughout IBM, these widgets provided a huge benefit to us.
Comparing Rational Functional Tester code with and without widgets
Now that we have all the pieces in place, we can illustrate the kind of efficiency gains produced by using the widget classes using a simple example. Suppose you wanted to automate the creation of a Team Space in the following IBM Workplace page:
Figure 5. IBM Workplace Team Space example
Using Rational Functional Tester's recording mechanism, you perform the following steps:
- Click in the Name field and enter a name.
- Click to select a Template.
- Enter description text in the Description text area.
- Click the OK button to create the Team Space.
You would then need to add lines of code for proper test verification and result logging. When completed, the Rational Functional Tester recorded code would look like this:
browser_htmlBrowser(document_ibmWorkplace(),DEFAULT_FLAGS) .click(atPoint(452,14)); button_newButton().click(); text_cdoName().click(atPoint(85,10)); browser_htmlBrowser(document_ibmWorkplace2(),DEFAULT_FLAGS) .inputChars("TestSpace1"); text_cdoName_textVP().performTest(); logInfo("Enter text: TestSpace1"); list_templates().click(atText("Discussion")); list_templatesVP().performTest(); logInfo("Selected Discussion Template"); text_nDescription().click(atPoint(33,13)); browser_htmlBrowser(document_ibmWorkplace2(),DEFAULT_FLAGS).inputChars ("This is a test space"); text_nDescription_textVP().performTest(); logInfo("Enter text: This is a test space into the Description text area"); button_oKbutton().click();
On the other hand, to produce the same test automation using the widget wrapper classes, you would simply enter the following five lines of code:
new Button(button_newButton()).click(); new TextField(text_cdoName()).setText("TestSpace1"); new ListBox(list_templates()).select("Discussion"); new TextField(text_nDescription()).setText("This is a test space"); new Button(button_oKbutton()).click();
This latter code example, using the widget wrapper classes, is far more intuitive and takes much less coding to produce the same test case. At IBM, we have built verification and logging directly into our widget classes, so those additional lines of code are not necessary when using our version of the widget classes.
Of course, this is a very simple example, involving only one very simple test case. Just imagine the gains in productivity and maintainability if you were writing hundreds of test cases to test the entire IBM Workplace feature area or thousands of test cases to test the entire product family!