One of the issues involved when attempting to use multiple testing tools; or when trying to migrate from one tool to another; is that each tool has devised a unique way of specifying or locating application components. Some tools, like IBM Rational Robot and Mercury's WinRunner, allow you to specify a simple string that contains information about the target component. Other tools, like IBM Rational RobotJ/XDE Tester, require that you provide a valid instance of a proxy "TestObject" or a similar component object--or the component itself--in order to perform an action on the component.
This document attempts to explain how the SAFS framework is evolving a standardized mechanism for specifying, locating, and then retrieving these component references for each tool-specific technology. It is important to note that this assumes the tool in use provides an API that allows you to retrieve and examine the component hierarchy of your application.
Goto: The Problem, SAFS Solution, Key Superclasses, A Walk-Thru.
Each GUI testing tool defines a unique mechanism for specifying and locating components in the tested application. Testers or frameworks wishing to exploit multiple tools, or migrating from one tool to another, must develop or maintain separate application-specific component maps for each testing tool in use. The extra effort for such development and maintenance causes testers or frameworks to avoid taking advantage of multiple tools; or from migrating from one tool to another.
Several years ago RRAFS (Rational Robot) became our first SAFS Engine. The technology used to identify components was simple text strings. There were no proprietary classes or objects involved and the syntax of the strings crossed environments (C, VB, Java, Web, etc..)
In 2003 we developed and deployed the RobotJ engine. This was our first STAF-based engine and was intended to augment--not replace--the existing RRAFS engine. In order to minimize the workload of test automators, we developed the classes and the algorithms that would allow our RobotJ engine to dynamically locate components using the same simple text strings used by Rational Robot.
These Java classes and algorithms can do the same thing when we go to develop new Java-based SAFS engines for different testing tools. The associated classes, files, and how to provide appropriate tool-specific subclasses are discussed below.
Note: These classes have not been around long, and some refactoring will occur. We are still in the process of refactoring and improving the implementation originally coded for Rational RobotJ. In some cases, functionality may exist in org.safs.rational.* subclasses that can and should be moved into the appropriate org.safs.* superclass.
So how do we get different tools to actually respond to the same type of recognition information? How does a string that works for IBM Rational Robot also work for IBM Rational XDE Tester, or Mercury's WinRunner, or JRex? Well, lets walk through an example.
We have a test record that tells us what window and what component we want to act on:
T, LoginWindow, OKButton, Click
Now, regardless of the GUI testing tool we plan to use, we setup "standard" recognition strings in our text-based App Map file--the one readable by SAFSMAPS:
[LoginWindow]
LoginWindow="Type=Window;Caption=Login"
OKButton="Type=JavaWindow;Caption=Login;\;Type=Pushbutton;Name=cmdOK"
Our hypothetical testing tool of choice is "GUITest" by GUISRUS, but this walk-thru applies to any gui testing tool.
GUITest can use component proxies. These are tool-specific objects that relay information and actions to the real application component. The tool provides an Object Map that allows user-defined names for these to be used in our tests. So, "LoginWindow" could be the user-defined name of such an object in the tool-specific Object Map. In our SAFS lingo, the Object Map is just a tool-specific App Map.
We CAN use the Object Map:
The quick GUITest version of AppMap lookup would be to take "LoginWindow" from the test record and retrieve the "LoginWindow" component from the GUITest Object Map. Which is fine if you have a predefined Object Map, if the names of those objects match the names in your tests, and you don't care to use any tool but GUITest. An augmentation of this is to still use a SAFS App Map file to map AppMap names to Object Map names like below:
[LoginWindow]
LoginWindow="LoginWindow"
OKButton="oddcmdOK3"
This can be beneficial if the tool's Object Map names change (shame on them!); or if maintenance in the tool is too difficult.
As mentioned, this means you have to create a tool-specific Object Map. And if the component you want to reference hasn't been captured in the Object Map, then you can't play with it. It is better to not have to make a tool-specific Object Map if we can dynamically find the objects at runtime. And that is where "standard" recognition strings come into play.
We CAN use "standard" Recognition Strings
Instead of making a tool-specific Object Map, and doing this again for every additional tool or platform; let's instead try an App Map with recognition strings and make that work for all tools. For this, we are back to using App Map A shown previously.
To make this work, we implement the Key Subclasses mentioned earlier for our GUITest tool.
A GTDDGUIUtilities or GTApplicationMap function could provide that Object Map lookup mechanism before resorting to the more dynamic lookup mechanism. You can have multiple lookup mechanisms, as shown below in VERY simplified pseudocode with little or no error detection.
Example of org.safs.guisrus.GTDDGUIUtilities:
public guisrus.GUIObj getGUIObj(String mapname, String windowname, String compname){
appmap = (GTApplicationMap) getAppMap(mapname);
// is the target the window?
if (windowname.equalsIgnoreCase(compname)){
// try to see if we cached it in our GTTestRecordHelper
guiobj = ((GTTestRecordHelper)trdata).getWindowGUIObject();
// if not cached in GTTestRecordHelper
if (guiobj == null) {
// perform primary lookup mechanisms
guiobj = appmap.findMappedParent(windowName);
// assuming found, cache it in GTTestRecordHelper for later use
((GTTestRecordHelper)trdata).setWindowGUIObject( guiobj );
}
return guiobj;
}
// otherwise the target is a child of the window
else {
// try to see if we cached it in our GTTestRecordHelper
guiobj = ((GTTestRecordHelper)trdata).getCompGUIObject();
// if not cached in GTTestRecordHelper
if (guiobj == null) {
// perform primary lookup mechanisms
guiobj = appmap.findMappedChild(windowName, compname);
// assuming found, cache it in GTTestRecordHelper for later use
((GTTestRecordHelper)trdata).setCompGUIObject( guiobj );
}
return guiobj;
}
}
The routine first attempts to locate a cached guisrus.guiobj that might have been stored in the current instance of GTTestRecordHelper. While that would be nice, more than likely we are not going to be that lucky. The GTTestRecordHelper may have this cached value reset at the beginning of processing for this new record. So odds are, we will be going down the findMappedParent path provided by our GTApplicationMap instance.
So, we are going to see what it takes to find the LoginWindow using the recognition strings in App Map A. We continue in GTApplicationMap.findMappedParent(). We start in tool-specific code because we are still searching for items stored in a tool-specific manner. But as you will see, we begin the use of predefined functions in superclasses to help locate the desired component.
Example of org.safs.guisrus.GTApplicationMap:
public guisrus.GUIObj findMappedParent(String windowname){
// check standard local ApplicationMap cache for the object
guiobj = (GUIObj) super.getParentObject(windowname);
if (guiobj != null) return guiobj;
// try any GUITest Object Map storage with the provided windowname
guiobj = GUITest.getObjectMapItem( windowname );
if (guiobj != null) return guiobj;
// retrieve the recognition string (or Object Map lookup value) from SAFSMAPS app map
windowPath = staf.getAppMapItem(mapname, windowname, windowname);
// try any GUITest Object Map storage again, this time with the app map lookup value
guiobj = GUITest.getObjectMapItem( windowPath );
if (guiobj != null) return guiobj;
// OK. We can't find a cached guiobj anywhere. Time for our dynamic SAFS search!
// instance our subclass of GuiObjectVector
guivector = new GTGuiObjectVector(windowname, windowname, windowPath);
// perform the search
guiobj = guivector.getParentGUIObject();
return guiobj;
}
We can now see how a GuiObjectVector is going to start us down the dynamic search path. Our GTGuiObjectVector subclass has a tool-specific getParentGUIObject() function, but the code will quickly be calling our predefined search algorithm.
Example of org.safs.guisrus.GTGuiObjectVector:
public guisrus.GUIObj getParentGUIObject(){
// perform any tool-specific prep here
// then jump right in to the superclass search
return (guisrus.GUIObje) getMatchingParentObject();
}
Example of "pseudo" org.safs.GuiObjectVector:
public Object getMatchingParentObject(){
// your GTGuiObjectVector subclass provides an array of all known toplevel parents (windows)
Object[] parents = getParentObjects();
Object child = null;
// see if one matches our recognition string
while(( !done )&&( child==null )){
// is the indexed parent a match?
if (! isMatchingParent(parents[parentIndex])) {
// no, try the next
parentIndex++;}
else {
// YES! return it
child = parents[parentIndex];
}
return child;
}
Example of "pseudo" org.safs.GuiObjectVector:
public boolean isMatchingParentObject(Object parent){
// your GTGuiObjectRecognition subclass answers the following queries
GuiObjectRecognition parentInfo = getParentGuiObjectRecognition();
// Is the parent showing/visible on the screen?
if(! parentInfo.isObjectShowing(parent)) return false;
// Does the parent class or a superclass match our recognition string Type?
if(! parentInfo.isMatchingClass(parent)) return false;
// Does the Caption match that of our recognition string?
return parentInfo.isMatchingCaption(parent);
}
As you can see, the generic superclasses will handle most of the algorithm logic that searches for matching components. The tool-specific subclasses mostly provide the information requested about a given object. The object is whatever the tool, or your subclass implementations, needs to use. This could be Strings, or things like Rational GuiTestObjects. The generic algorithm simply passes them along as needed.
The hunt for matching child objects is essentially the same. Once we match the parent window, we ask for arrays of child objects and process those in the same manner. That is, we loop through the children and query your subclasses to provide the necessary information, or the true or false answers.
Reviewing the Rational RobotJ subclass implementations may (or may not?) make this easier to digest. They have the necessary implementation, but they also have additional functions used by the Rational implementation that would not be necessary for other engines.