An Object-Oriented framework for IBM Rational Functional Tester

Functional Tester enables test teams to implement GUI automation which is truly object oriented in design. By using a hierarchical framework which enforces this design, testers can gain huge benefits in both ease of script writing and ease of maintenance.

Share:

Tim Snow (tsnow@us.ibm.com), Technical Lead, IBM

Tim SnowTim Snow leads the Automation Services team of the Notes Client. For the past two years, he has spearheaded the conversion to IBM Rational Functional Tester. Tim has a diverse background, ranging from programming in Java and C++, to designing expert systems, to a Ph.D. in analytic philosophy.



Amy Groves (grovesa@us.ibm.com), Knowledge Content Architect, IBM

Amy GrovesAmy Groves is currently the Knowledge Content Architect for the IBM QSE team. She has worked for nine years enabling and promoting GUI automation, and has provided internal consulting services on such topics as test automation and software development best practices. Amy received a Master of Science in Information Systems from Northeastern University.



Chris Carlson (chrisrc@us.ibm.com), Lead Technical Architect, IBM

Chris CarlsonChris Carlson is the Lead Technical Architect for IBM Enterprise Technical Learning. She helps provide planning strategy to make IBM-wide technical learning more relevant, up-to-date, and pervasive. Prior to that, she was a Test Automation expert with the IBM Test Community Leadership (now called QSE). Before joining IBM, she was a Spacecraft Instrumentation Engineer at NASA. She has an MS in Chemical Engineering from the University of Illinois at Urbana.



24 January 2006

If you are a tester or test manager, then you are likely drawn to GUI automation because it promises to provide more efficient and expandable testing, a reduction of point-and-click tedium, and a shorter test cycle. Most automation tools enable testers to simply record a set of GUI interactions and play them back against the application under test. And yet, even in companies which have made a considerable up-front investment in tools and training, the automation scripting often becomes shelfware. This begs the question: why does this happen?

The answer lies in the record-playback model itself. Although record-playback features can be used to create suites of test cases very quickly, the limitation of this approach is that the application under test must be mostly complete and functioning correctly before a workable script can be recorded. Testers are thus effectively limited to using GUI automation for regression testing -- that is, making sure that functionality that worked yesterday still works today. Furthermore, the record-playback approach leads to extensive testcase maintenance. Teams end up recording, and re-recording, and re-recording scripts each time the application changes, until they finally give up on the testcase maintenance and shelve the tool altogether.

Many test teams have addressed these problems by abandoning the record-playback model and writing test cases manually instead. The success of this approach has varied, depending on the model the automation tool uses for object recognition. In some tools, the object recognition algorithms are complex and inaccessible, making updates to the scripts extremely tedious and in some cases impossible. In other tools, the object recognition algorithms are exposed and simple, which makes updates much more manageable, but which also has the unfortunate side effect of making object recognition less reliable.

In contrast to these other tools, IBM® Rational®® Functional Tester's (RFT) object recognition algorithms are both complex (making object recognition quite robust) and accessible (via the object map). This unique object recognition scheme, coupled with RFT’s support of Java™ as a scripting language, has enabled -- for the first time -- a truly object-oriented (OO) approach to writing GUI automation. This approach enables you to generate scripts early in the development process, based on project requirements or developer specifications, instead of waiting until an application is complete and then using record-playback. Additionally, RFT’s OO design, and the reusable code that it promotes, makes it easy for you to update object recognition properties to reflect changes in the evolving application without touching other parts of the code. Using RFT, then, you are able to write flexible, reusable Java code that is both robust and easy to maintain.

To take full advantage of this, IBM's Quality Software Engineering (QSE) team collaborated with test professionals throughout the corporation to design a common architecture for RFT. This architecture consists of three tiers:

  • “Appobject” classes that logically group GUI elements together. These classes consist of discrete, granular private object maps that contain a small number of related GUI elements, along with routines that provide access to these elements.
  • Tasks or methods, that that execute commonly-travelled paths through the GUI.
  • Test cases that invoke the tasks, verify the state of the application, and log the results.

We have instantiated this architecture by creating a framework that consists of three folders: appobjects, tasks, and testcases. Each of these folders is a package within an RFT project, and represents one tier of the three-tiered architecture. This article will explain the purpose of each folder, in turn, below.

The appobjects folder

One important advantage to using the QSE Framework is that it helps you keep object map maintenance under control. Object maps get unruly when they get too large, so it is difficult to use shared object maps for large application testing. On the other hand, creating a private object map for each test script leads to having the same object defined in several different object maps. This is a problem, since all of these object maps will have to be updated any time the GUI changes.

The QSE approach was devised in order to avoid this dilemma. It does so by separating the object maps from the test cases. Object maps are located in scripts whose sole purpose is to return the objects to the caller. In this way, the object maps can be private within the script that holds them, and it is possible to store information about each object in the application in one and only one map.

In the QSE Framework, the object maps reside in the appobjects folder, the first tier of the framework's three-tiered architecture. The purpose of this folder is to hold scripts that return the GUI objects contained in the application. Each script in this folder includes a private object map and several methods that simply return objects to be used by other scripts.

For very simple applications, a single appobjects script might correspond to a page in the application. A static Web page, for instance, could have one script that holds all of the objects on the page in its map, and one method for each object, the sole purpose of which is to return a single object on the page. For example, suppose you need to log on to Yahoo®, and the login screen consists of the very simple Web page shown in Figure 1 -- a standard Web form.

Figure 1. The Yahoo login page
The Yahoo login page

In this case, you would create an appobjects script corresponding to this page, and create methods in this script that merely return each of the objects on the form. Your appobjects script might look like that in Listing 1.

Listing 1: An appobjects script
public class LoginPage extends LoginPageHelper
{
     public GuiTestObject getText_LoginID() {
          return Text_LoginID(ANY, NO_STATE);
     }

     public GuiTestObject getText_Passwd() {
          return Text_Passwd(ANY, NO_STATE);
     }
     public ToggleGUITestObject getCheckBox__PersistentID() {
          return CheckBox__PersistentID(ANY, NO_STATE);
     }
     
     public GuiTestObject getButton_SignIn() {
          return Button_SignIn(ANY, NO_STATE);
     }
}

Test scripts higher up in the framework's hierarchy, in the tasks or testcases tier, could now call your appobjects methods to get these objects, and then manipulate the objects in order to log on to the application.

Using the traditional approach, you would need to include these login fields in every script that had to log in to the application, either in each of the script's private object maps or in one huge shared object map. However, using the QSE approach, each object is isolated into one and only one small, manageable object map. Thus, when one of these objects changes, you can easily locate the object in a single small object map, rather than in many private maps or one giant shared map. This makes object map maintenance much easier.

Of course, real-world applications are not as simple as this first example. Typically, there will be collections of objects that remain the same across different pages of the application. Such collections include tabs or banners. Each of these should get its own appobjects scripts with a private object maps. Then, any leftover objects that are only on one particular page can be collected in an object map for that particular page.

For example, the Yahoo mailbox looks like Figure 2.

Figure 2. The Yahoo mailbox
The Yahoo mailbox

But when you click on the Inbox link, you get the screen shown in Figure 3.

Figure 3. The Yahoo inbox
The Yahoo inbox

The only significant change to the page occurred in the lower right quadrant of the screen. The header at the very top, the tabs, the Check Mail and Compose buttons, and the Folders Navigator all remained the same.

Similarly, if you click on the Calendar tab, you will see the screen in Figure 4.

Figure 4. The Yahoo calendar
The Yahoo calendar

Here, you still have the Header, its associated links, and the tabs, but the buttons and Navigator have disappeared.

This should give you pause before splitting this application into appobjects that correspond to each page of the application. The problem is that if you create an appobjects script for the Mailbox page, and another for the Calendar page, you will have two private object maps, each of which contains links for navigating the tabs. That means that any time these tabs change (for example, the developers decide to make them buttons instead of links), then you'll have to change your test harness code in two places. Furthermore, since these tabs appear on every page in this application, there will be hundreds of object maps containing these same tabs -- if you follow this flawed strategy.

Instead, you should identify those collections of objects which remain the same across different pages. You can then group these together into their own appobjects script, with their own private object maps. Then, when one of these items changes, you can easily locate it and make the appropriate changes to the map, and those changes will be valid for all of the pages on which these tabs occur.

Figure 5 illustrates the collections of objects that are common across pages.

Figure 5: The Yahoo mailbox with suggested appobjects groupings
Suggested appobjects groupings

So, your this analysis reveals that there are three collections of objects that are shared between various pages:

  1. The header with tabs at the very top of the page
  2. The two buttons below the header
  3. The navigator to the left

Since these headers and toolbars appear on several pages, it is smart to give them each their own object map. The lower right quadrant, on the other hand, is the part of the page that is not shared with other pages, so it should get its own map.

Therefore, create four scripts in the appobjects folder for this page:

  • A script called HeaderWithMainTabs that holds the header and the primary set of tabs across the topmost part of the screen
  • A script called MailButtons that holds the buttons
  • A script called MailNavigator that holds the links on the left hand side of the screen
  • A script called WelcomePage that holds the rest of this page's GUI objects, those which are not shared with other pages

Each of these scripts should contain methods that return the objects from its map.

For example, the MailButtons class might look like Listing 2.

Listing 2: The MailButtons script class
public class MailButtons extends MailButtonsHelper {

     public GuiTestObject getButton_CheckMail() {
          return Button_CheckMailbutton(ANY, NO_STATE);
     }

     public GuiTestObject getButton_Compose() {
          return Button_Composebutton(ANY, NO_STATE);
     }
     
     public GuiTestObject getButton_SearchMail() {
          return Button_SearchMailbutton(ANY, NO_STATE);
     }
}

After splitting up this first page into separate object maps, you should move on to the other pages in the application. For instance, clicking the Inbox link reveals more links to particular messages (see Figure 3), but the header, buttons, and Navigator remain the same. So all you need to do for this page is to create one more page called Inbox. You can then proceed by clicking Addresses, and continuing to add scripts for collections and pages that you have not already addressed. Finally, you will want to create a separate script that contains the browser and browser toolbar buttons (Back, Refresh, and so on), so that you can access these objects from anywhere in your project.

As you can see, you may need to create many object maps to describe a single page. One of the easiest mistakes to make is to assume that because one object appears on the same page as another object, both objects should reside in the same object map. As you have seen, this is often not the case. However, because it is still true that each object will reside in only one object map, maintaining your scripting will be relatively easy.

Note: when implementing this idea, we found that there was a great benefit to returning our widget classes rather than GuiTestObjects or TestObjects. The widget classes wrap TestObject interfaces, exposing and simplifying commonly used methods.

The tasks folder

The tasks folder is the middle tier of the QSE Framework’s three-tiered architecture. Tasks methods invoke appobjects methods in order to gain access to GUI elements in the application. In turn, tasks methods are invoked by test cases. The strengths of the tasks folder are that it promotes code reuse and shields the test cases from low-level implementation details. A robust and well-designed tasks layer, along with well-constructed object maps in the appobjects layer, is critical to the success of the entire automation effort.

Typically, tasks navigate through the application, while test cases verify conditions and log results. Most tasks methods exercise commonly-traveled paths. For example, for the Yahoo! mail application, you will undoubtedly want to write tasks to send an e-mail message, find and open an e-mail message, delete an e-mail message, and so on. Other tasks manipulate or query complex, custom, or unstable GUI elements. The Yahoo! calendar control is an excellent example of a GUI element that may need to be manipulated and queried via tasks.

Creating an insufficient number of tasks will greatly retard test case writing and, by missing out on the benefits of code reuse, increase maintenance. On the other hand, writing tasks which are too granular is cumbersome. You should not create a task to manipulate or query each simple GUI element on the screen; if you do so, you might as well call directly into the appobjects layer. When designing your tasks folder, try to keep object orientation in mind, targeting both abstract objects (for example, mail messages) and concrete objects (for example, calendar controls) that need attention.

Where appropriate, a task may return a Boolean value to the test case, to indicate whether or not the task has executed successfully. In other cases, a task may return data that is needed by the test case. If an unexpected error condition occurs, a task should throw an exception that must be caught and handled at the test case level.

The grouping of task methods into classes should be done in an intuitive manner. It might be possible to write only four classes to accommodate Yahoo! Mail, Addresses, Calendar, and Notepad. However, since Mail and Calendar will probably become large and unwieldy, you may wish to deconstruct the class to a further level of detail. For instance, you might break Mail into classes such as MailCompose (to compose and send messages), MailFolders (to open and query the Inbox, Draft, Sent, and Trash Folders), MailList (to perform operations and query the list of messages), MailActions (to perform menu picks off of the action buttons and verify the related string resources), and so on. For large and complex applications, you may find it advisable to nest folders within the tasks folder for a more intuitive organization.

The code shown in Listing 3 implements a MailCompose class containing two tasks. Note the instantiation of the appobjects classes MailButtons and ComposeMailPage. Note also that the tasks return Boolean boolean values to promote error checking. Finally, note that it is possible for one task to call another task, to promote code reuse and reduce the number of lines of code overall.

Listing 3: MailCompose code The MailCompose class
public class MailCompose extends MailComposeHelper
{
     MailButtons mailButtons = new MailButtons();
     ComposeMailPage message = new ComposeMailPage();
     
     /**
      * Composes a message in Yahoo mail.
      */
     public boolean composeMessage(String sTo, String sCC, String sBcc, 
          String sSubject, String sBody)
     {
          mailButtons.getLink_Compose().click();
          
          WTextField tfTo = message.getText_To();
          
          if (!tfTo.waitForExistenceBoolean()) {
               logError("Could not create new message. Cannot continue.");
               return false;
          }
          
          tfTo.setText(sTo);
          message.getText_Cc().setText(sCC);
          message.getText_Bcc().setText(sBcc);
          message.getText_Subj().setText(sSubject);
          message.getText_Body().setText(sBody);
          
          return true;
     }

     /**
      * Composes and sends a message in Yahoo mail.
      */     
     public boolean composeAndSendMessage(String sTo, String sCC,
          String sBcc, String sSubject, String sBody)
     {
          if (!composeMessage(sTo,sCC,sBcc,sSubject,sBody))
          {
               logError("Could not create new message. Cannot continue.");
               return false;
          }
          message.getButton__Sendsubmit().click();
          
          return true;
     }     
}

The testcases folder

As the topmost tier of the QSE Framework’s three-tiered architecture, the testcases folder provides the most general view of the test effort. Generally speaking, test cases invoke tasks (passing data if necessary), verify conditions, and log results. Test cases should contain only the simplest logic and flow of control; all else is reserved for the tasks folder. If sufficient time and thought has been devoted to the construction of the tasks folder, it should be quick and easy to generate test cases, even for a relatively inexperienced coder.

True, even the most robust tasks folder can only be expected to describe frequently-traveled paths through the application. It may occasionally be necessary for a test case to call an appobjects method directly, rather than via a task. This practice should only be considered a problem if it is happening too frequently, with the result that the test cases become too complex and require an undue amount of maintenance.

In the testcases folder, each class should represent a feature area or other logical, intuitive grouping, and each method should represent a test case. This design makes it very easy to write data-driven testing, thus increasing test coverage with minimal effort.

The code in Listing 4 implements a SendMail class containing a sendMailWithTextBody test case. Login is a class from the tasks layer; note its instantiation, as well as the instantiation of the MailCompose class, within the test case. The test suite, defined in testMain(), calls the test case many times, passing different data each time. Error checking is performed throughout, and all possible exceptions are caught and handled.

Normally, the string literals would be resourced to a .properties file or other external data source. Here, the strings are left intact for the purposes of illustration. Also, under normal circumstances, a testcases class would contain not just one but many test cases.

Listing 4: The SendMail class
public class SendMail extends SendMailHelper
{
     /**
      * Data-driven testcase for testing sending mail.
      * Sends mail to addressees and verifies that mail shows up in Sent folder.
      */     
     public boolean sendMailWithTextBody(String sTo, String sCC, String sBcc,
          String sSubject, String sBody)
     {
          if (!new Login().login())
               return false;
          
          MailCompose message = new MailCompose();
          if (!message.composeAndSendMessage(sTo,sCC,sBcc,sSubject,sBody))
               return false;
          
          if (!message.verifyMessageInSentFolder(sSubject))
               return false;
          
          return true;
     }

     /**
      * Runs a full regression test of this feature area.
      */
     public void testMain (Object[] args) 
     {
          String sTestHeader;  
  
          //Send Mail with Text Body
          sTestHeader = "Send Mail, Text Body, one to: recipient";
          try {
               logTestResult(sTestHeader, sendMailWithTextBody("iristest2004@yahoo.com",
                    "", "", "Test one to: recipient", "Hello There"));
          } catch (Exception e) {
               logTestResult("Exception in " + sTestHeader, false, e.getMessage());
          } 
          
          sTestHeader = "Send Mail, Text Body, one to: and one cc: recipient";
          try{
               logTestResult(sTestHeader, sendMailWithTextBody("iristest2004@yahoo.com",
                    "iristest2004@yahoo.com", "", "Test one cc: recipient", "Hello There"));
          } catch (Exception e) {
               logTestResult("Exception in " + sTestHeader, false, e.getMessage());
          } 
            
          sTestHeader = "Send Mail, Long Text Body";
          try {
               logTestResult(sTestHeader, sendMailWithTextBody("iristest2004@yahoo.com",
                    "", "", "Test long text body", "Lorem ipsum dolor sit amet, consectetur
                    adipiscing elit, set eiusmod tempor incidunt et labore et dolore magna
                    aliquam.”));
          } catch (Exception e) {
               logTestResult("Exception in " + sTestHeader, false, e.getMessage());
          }
          
          //etc.
  
     }
}

Conclusion

Truly useful, low-maintenance GUI automation has long been the Holy Grail for testers. Until now it has been unachievable. Standard record-playback methods recommended by product manufacturers were not easy to maintain, and manual scripting was limited by inaccessible or overly simple object recognition technology. Now, however, with the arrival of Rational Functional Tester, the quest can be completed. RFT’s unique advantages provide the opportunity to develop a truly OO implementation of GUI automation.

By using a framework that enforces this object-oriented approach, you can gain huge benefits in both ease of script writing and ease of maintenance. IBM test teams across the globe have adopted this framework, and it has been an important part of the success of GUI automation at IBM. We hope that you find the principles and ideas described in this article helpful, and that you are now contemplating implementing your own multi-tiered, object oriented, RFT architecture.

Resources

Learn

  • For more information about Rational Functional Tester, visit its product page.

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Rational software on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Rational
ArticleID=101804
ArticleTitle=An Object-Oriented framework for IBM Rational Functional Tester
publish-date=01242006