Implementing a GUI Layer with Classes
Posted by admin - Dec 20, 2008 Articles, Meir Bar-Tal 0 1 Views : 1484 Receive Updates For This Category
Article Tools
- Print this page
- Add Comment
- Send to Friend
- Last Updated on :
Aug 29, 2011
A Russian version of this article is available here
Abstract
This article describes a powerful technique that exploits Object Oriented Design Patterns, QTP descriptive programming (DP) and the Dictionary object to pack together GUI objects with their corresponding business oriented functions. The article includes a valuable bonus: a highly effective tip that enables to exit a test smoothly, preventing QTP from getting stuck when it fails to identify GUI objects during runtime.
Introduction
A central issue in automation development is how to reduce the effort spent on maintenance. Questions such as: “Should we use an Object Repository (OR) or Descriptive Programming (DP)? if an OR is chosen, then should we use a shared OR or a local OR for each Action? If DP is selected, then what is the optimal way to implement it?” are quite ubiquitous and the answers given can vary depending on the particular characteristics of the project, but many times also on the individual personalities involved.
In this article I will analyze the concept of Test Objects in the context of Object Oriented principles. I shall attempt to show that QTP’s Object Repository does not fit well with such principles and what is the impact of this on automation development viz-a-viz cost-effectiveness and maintainability. Subsequently, I will describe an expansion of the OR concept that is compatible with OO principles – the GUI Layer. This concept has been adopted by SOLMAR’s automation experts based on the observation that it is possible to maximize code reusability (and thus boosting maintainability) by undertaking an OO approach that breaks an automation project into several abstraction levels.
Test Objects and Run-Time Objects
A test object encapsulates a reference to a run-time object – the real world object that is instantiated in the application under test (AUT). A test object stores the description of the referenced object, which is a set of attributes and values that serve as criteria to locate the right object during the test run session. This description is pretty much like the one we keep in mind to identify our blind date partner at the meeting time (“I’m brunette, slim and tall, and will be wearing a red dress and shoes”, you’ve been told on the phone).
Similarly, QTP uses such a description to query the OS if there’s an object currently loaded to the machine’s RAM. If the answer is positive, the OS returns the address of the object (a reference or pointer) and hence QTP gains access to the object’s public methods and properties, since the private methods and properties are hidden from view by definition (so QTP never makes progress to the private domain, as you would with the brunette, if lucky!).
Though this description may seem overly simplified, basically it is this process that enables us to interact with the GUI (and other) objects using QTP or other automation tools (Test Partner, Test Complete, Rational Robot, AutoIt, etc.). A more detailed account of the distinction between Test Objects and Run-Time Objects is available in Yaron Assa’s article “Differences and Connections between Runtime Objects and Test Objects” (2009).
Test Objects and the Object Repository
The above account of test objects makes it seem only natural to think of them as data entities. What do I mean by this? Based on a description composed of a set of properties and values, QTP is able to identify a GUI object among all the objects currently loaded to the machine’s RAM. This is similar to the retrieval of a particular record from a database table based on a SQL WHERE clause. The difference is that in this case, it is the OS (Windows), not the DB, the one that returns the record. Moreover, this record contains a single field that stores a reference to the actual runtime object. Following this line of thought, it seems only natural to have these entities – Test Objects – stored in a database, which essentially is exactly what the Object Repository is (a side note, if you noticed the bdb suffixed files in QTP tests, then note that bdb actually stands for Berkeley Data Base – an Oracle product).
What, then, seems to be the problem with the OR approach? Tests Objects are really data entities, but at the end of the day we need to perform actions on Runtime objects, insomuch that the above account leaves us with an incomplete picture. I will explain below why.
It is true that the OR, if used properly, reflects a best practice according to which we ought to store the description of each GUI (Test) Object only once to reduce maintenance costs. However, in order to achieve this a great effort must be invested also into managing the automation project properly, because ad-hoc requests and resource limitations can endanger the integrity of such a precious resource. For instance, suppose that two automation developers work simultaneously with a shared repository, but one is maintaining the scripts for the last version and the other is developing new scripts for the newest one. Given that GUI changes are made quite regularly from one AUT version to the next, it will not be too long until the OR becomes cluttered with new, redundant, test objects required by the developer working on the newest version. And this is a relatively good result. The worst case would be one where each developer would change in turn the descriptions of existing objects to match their requirements, damaging irrevocably the ROI on the automation project as a whole. Of course, there are solutions to such situations, such as keeping separate versions of the OR that match each AUT version and using configuration management software in the automation project. But, as aforesaid, this requires good project management, which is not always available.
Moreover, keeping versions of the OR leaves one issue still open. Because automated tests are, in general, regressive by nature, then maintenance would certainly pertain not only to the OR but also that the code that implements the actual GUI manipulations and verifications, as well as the input data and expected results data. If the common situation would have been that only the object descriptions changed from one AUT version to the next, then using an OR would be an excellent solution. However, more often than not, GUI changes reflect modifications in the way the AUT actually functions, which are many times also accompanied by even deeper changes – in the database structure, for example. So, changes in the AUT would require matching changes in both the OR and the scripts that refer to its test objects, as well as in the data sources that feed these scripts. Taking into account the fact that scripts are most of the time maintained, not developed, the aforementioned line of analysis lead me to the conclusion that something in the OR concept is faulty regarding the manageability of a large-scale automation project.
The Object Oriented View
The Object Oriented software paradigm is based on the basic concepts of encapsulation, inheritance and polymorphism. Encapsulation basically means to pack together functionality and data structures that belong together, and the package through which we achieve this is called a class. In fact, classes mostly are representations of data entities (like Customer, Product, etc.). The difference between a fully featured class and a mere data structure (as in the C programming language) is that the class also wraps the functions available for use with the class data fields. These are commonly referred to as the class methods. For example, a typical class for a Customer would pack the data fields that define the customer as a data entity (customerId, firstName, lastName, phoneNo, etc.) together with functions such as setCustomerId, getCustomerId, which assign and retrieve the value of the respective field, as well as others like getCustomerAge and getCustomerBalance which would perform a calculation based on the class fields current values and then return the result.
Inheritance is deeply related to one of the major goals of the OO approach – reusability. In OO programming languages like C++ and Java, reusability comes into play by means of the ability to device a new class that “inherits” the fields and methods of a previously defined class (also called a base class) and expands on these according to the specific requirements that raised the need for the new class. Polymorphism is yet another powerful concept that enables one to design more than one version of a function (with the same name but different number or types of arguments) to be able to handle transparently different situations within the application context. For instance, we might need to handle numbers of different types (float, int) and so instead of having a single function with conditional statements that check for the value type that was passed, we’ll have several functions with the same name but different signatures (different type of arguments). Hence in our code that uses these functions we would not need to use casting; we would use the same interface in both cases, with the correct process ultimately invoked by the runtime environment. However in QTP these two last concepts of inheritance and polymorphism cannot be implemented based on the VB Script language which provides very rudimentary support for working with classes. Nevertheless, we shall see later that this is not a reason to refrain from using classes in testing automation.
The public and private notions mentioned above are of great importance with regard to encapsulation and inheritance. They enable the code developer to determine which fields (also called properties or attributes of the class) and methods will be actually accessible to the world outside the class (public), and which will remain for internal (private) use of the class members (i.e., the class fields and methods). The OO methodology is not new and has been widely put into practice in the software design and development industry. It is recognized as one approach that makes the resulting code more reusable, maintainable, readable, scalable, extensible and, yes, also more testable (as huge pieces of functionality are broken down into smaller, self-contained capsules or packages). Of course, making good use of the methodology requires deep analytical skills that enable the code designer to infer correctly from the requirements of the software under construction which are the appropriate entities involved and how they are interrelated.
How this relates to the way we approach, or should approach, the testing automation challenge? Let us delve into it below.
Testing Automation and Software Development
One thing I have witnessed throughout my career is that QA professionals of all levels treat testing automation as just another testing activity. Not surprisingly, it is not rare to find automation professionals that think likewise since many times the role of automation. Sometimes this is even reflected by the job title given to the automation staff: “automatic tester” is one of the absurd titles I have encountered several years ago. This reflects a misunderstanding of the role of the Automation Developer and, as I shall explain in what follows, this belief is a true misconception of the essence of testing automation.
The task of automating tests should not be treated differently than any other content specific automation task. Generally speaking, computer programs that perform operations instead of humans implement automation. So basically any block of code is a kind of automation device, or robot. Scripts that carry out operations on GUI objects – such as QTP tests – are not different than any other piece of software. Put it in other words, a testing automation project is, indeed, a specific kind of software development project. As with any software product, an automation project also has its own SRS (Software Requirements Specifications) document: the STD (Software Test Design) “document” (or tests design in a quality management tool such as HP’s Quality Center or Orcanos’ QPack), which should guide the automation developer in the implementation of the required code.
If so, then an automation project should be treated and managed as any other development project and, in fact, even more so. This is because the automation developer faces challenges that are usually not felt by the developers of the AUT. Some of the main challenges are the following:
-
First, the automation developer must have an overall view of the AUT, as the automation scripts may cover a large part of the AUT’s main functionality. Members of the AUT’s development team do not necessarily need this, as the division of labor among the different teams is typically coordinated by the team leaders and a project manager. So a developer can focus on the part assigned to him and do it well even without having in depth knowledge of the whole system.
-
Second, the automation scripts should be mapped to the test design, as they should emulate the steps done by a human tester. But, quite often, automation developers find out that the test design leaves many open issues that need to be resolved beforehand, as the automation scripts do not possess the flexibility and ingenuity of a human tester during runtime.
-
Third, the GUI controls used by the development team, and also the AUT’s behavior may pose a real technological challenge regarding the identification of the objects by QTP (or other tools). Many times solutions that extend the basic capacities of the tools are required, as is the case with third-party and custom controls.
Following the above exposition, I think we may conclude that an automation project should be definitely managed as a development project. If this is so, why should one fall again and again victim to the fallacy of treating an automation project as a trivial task (does “record-replay” remind you of something?), where the opposite is true? Why not, then, approach to do it using the most widely accepted development method – Object Oriented – to attain the best possible results? In what follows I will describe a method to implement automation “scripts” based on an extension to the OR concept – the GUI Layer, which is based on solid OO principles.
The Concept of Layers
I hope to have managed so far to make clear why the OR approach, with scripts spread all around the place, is far from being the optimal approach. Now, following the above discussion, let us take a look at the concept of developing code in layers, especially the GUI Layer concept. In general, layers are useful to maximize reusability (recall the previous discussion on OO principles). I shall define a GUI Layer as a set of classes that pack (encapsulate) together the interfaces required to manipulate the AUT’s GUI objects in each application context. In other words, it is a set of classes that bridge between the AUT’s GUI (i.e., the test objects proper) and the Business or Application Layer, of which we shall have more to say later. In a sense, may be it would be more appropriate to call it the GUI-Business Bridge Layer, but it is already accustomed among the experts to call it a GUI Layer, to keep it short. I will illustrate below how such a layer can be built, and what are the benefits gained from adopting such an approach to testing automation.
Implementing the GUI Layer
Encapsulating Test Objects in Classes
Let us take a testing automation project on a typical AUT and see how the solution should be designed according to the approach outlined above. The first step would be to make a simple list of all application GUI contexts – i.e., the windows (pages in a Web application), dialogs, and popup messages. For each of these entities, which are containers of other GUI objects, we define a class, for example:
Class Login
End Class
Class MainWindow
End Class
Class CreateCustomer
End Class
and so on for each of the application contexts. Because QTP does not allow for direct instantiation of classes defined in external vbs files with the operator New, it is also required to define the following function that will return an instance of a GUI layer class (a kind of constructor function), as follows:
'-------------------------------------------------------------------------------
Public Function CreateLogin()
'-------------------------------------------------------------------------------
'Function: CreateLogin
'Creates an instance of the Login class
'
'Remarks:
'
'Arguments:
' N/A
'
'Returns:
' Object - As Login
'
'Owner:
' John Doe
'
'Date:
' dd-MMM-yyyy
'
'-------------------------------------------------------------------------------
Dim objLogin
Set objLogin = New Login
Set CreateLogin = objLogin
'-------------------------------------------------------------------------------
End Function
'-------------------------------------------------------------------------------
The second step is, obviously, to define the members of each class. Now, since each class is, as aforesaid, a container of other GUI objects, we shall make use of the Scripting.Dictionary to store the references to the test objects contained in the window, dialog or page. (the Dictionary object has been already extensively discussed in other articles published at AdvancedQTP‘s knowledge base). So, the first member I shall introduce here will be common to all GUI Layer classes, and I will define it as m_htChildObjects, for example:
Class Login
Private m_htChildObjects 'As Scripting.Dictionary
End Class
Class MainWindow
Private m_htChildObjects 'As Scripting.Dictionary
End Class
and so on for each of the application contexts (ht stands for HashTable, which is what the dictionary really is). The private member m_htChildObjects will be accessed through the class property ChildObjects. This property is defined as follows:
'-------------------------------------------------------------------------------
'Property: ChildObjects
'Get and Set the m_htChildObjects member field
'
'Remarks:
' R/W
'
'Arguments:
' dic
'
'Returns:
' m_htChildObjects As HashTable
'
'Owner:
' John Doe
'
'Date:
' dd-MMM-yyyy
'
'-------------------------------------------------------------------------------
Public Property Get ChildObjects()
'-------------------------------------------------------------------------------
Set ChildObjects = m_htChildObjects
'-------------------------------------------------------------------------------
End Property
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
Public Property Let ChildObjects(ByRef dic)
'-------------------------------------------------------------------------------
Set m_htChildObjects = dic
'-------------------------------------------------------------------------------
End Property
'-------------------------------------------------------------------------------
The third step is to define the objects contained within each context. For this purpose, I will define a public method within the class called Init, as follows:
'-------------------------------------------------------------------------------
Public Function Init()
'-------------------------------------------------------------------------------
'Function: Init
'Initializes the context and child objects
'
'Dependencies:
' IsContextLoaded(htContext)
'
'Remarks:
' N/A
'
'Arguments:
' N/A
'
'Returns:
' True/False
'
'Owner:
' John Doe
'
'Date:
' dd-MMM-yyyy
'
'-------------------------------------------------------------------------------
ChildObjects = CreateObject("Scripting.Dictionary")
With ChildObjects
.Add "Browser", Browser("name:=My App")
.Add "Page", ChildObjects("Browser").Page("title:=My App \- Login")
.Add "Username", ChildObjects("Page").WebEdit("html id:=Username")
.Add "Password", ChildObjects("Page").WebEdit("html id:=Password")
.Add "Submit", ChildObjects("Page").WebButton("outertext:=Submit")
End With
'IsContextLoaded is a function that iterates through the Dictionary and checks if the GUI objects "exist"
Init = IsContextLoaded(ChildObjects)
'-------------------------------------------------------------------------------
End Function
'-------------------------------------------------------------------------------
The code snippet above shows a typical Init method for a GUI Layer class for a Web application Login page. The test objects are added as entries to the ChildObjects Dictionary, and their identity is defined using Descriptive Programming (DP). The reader can easily infer the analogy to the OR. It is thanks to this encapsulation method that we ensure that GUI objects are always defined at a single place. At the end of the function body you will notice that it returns the result of a call to the function IsContextLoaded which accepts as argument the Dictionary that stores the ChildObjects.
IsContextLoaded is defined in a separate common library, as follows:
'-------------------------------------------------------------------------------
Public Function IsContextLoaded(ByRef htContext)
'-------------------------------------------------------------------------------
'Function: IsContextLoaded
'Checks that the current GUI context is loaded
'
'Iterates through the htContext (HashTable) items and executes the Exist method with 0 (zero) as parameter.
'
'Remarks:
' N/A
'
'Arguments:
' ByRef htContext - As HashTable
'
'Returns:
' True/False
'
'Owner:
' Meir Bar-Tal, SOLMAR Knowledge Networks Ltd.
'
'Date:
' 11-Nov-2008
'
'See Also:
'
'-------------------------------------------------------------------------------
Dim ix, items, keys, strDetails, strAdditionalRemarks
'---------------------------------------------------------------------------
items = htContext.Items
keys = htContext.Keys
For ix = 0 To htContext.Count-1
IsContextLoaded = IsContextLoaded And items(ix).Exist(0)
strDetails = strDetails & vbNewLine & "Object #" & ix+1 & ": '" & keys(ix) & "' was"
If IsContextLoaded Then
intStatus = micPass
strDetails = strDetails & ""
strAdditionalRemarks = ""
Else
intStatus = micWarning
strDetails = strDetails & " not"
strAdditionalRemarks = " Please check the object properties."
End If
strDetails = strDetails & " found." & strAdditionalRemarks
Next
'---------------------------------------------------------------------------
Reporter.ReportEvent intStatus, "IsContextLoaded", strDetails
'-------------------------------------------------------------------------------
End Function
'-------------------------------------------------------------------------------
And it returns True if all objects defined in the Dictionary are identified, or False if at least one object is not found. This function is generic and it is used in the projects I manage to ensure that QTP does not get stuck while attempting to perform some operation on a non-existing GUI object. Another benefit of this method is that it points exactly to the object we need to recheck and update, making maintenance much easier.
Encapsulating Business Methods in Classes
The next step after defining the child objects of the GUI context is to define the operations required to perform the application or business scenarios within the given context. This is easily done by implementing class methods. For example, the login class outlined above would need the following methods to begin with: SetUsername, SetPassword and Submit. These are shown below:
'-------------------------------------------------------------------------------
Public Function SetUsername()
'-------------------------------------------------------------------------------
'Function: SetUsername
'Set the Username field
'
'Dependencies:
' N/A
'
'Remarks:
' N/A
'
'Arguments:
' N/A
'
'Returns:
' N/A
'
'Owner:
' John Doe
'
'Date:
' dd-MMM-yyyy
'
'-------------------------------------------------------------------------------
ChildObjects("Username").Set GlobalDictionary("Username")
'-------------------------------------------------------------------------------
End Function
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
Public Function SetPassword()
'-------------------------------------------------------------------------------
'Function: SetPassword
'Set the Password field
'
'Dependencies:
' N/A
'
'Remarks:
' N/A
'
'Arguments:
' N/A
'
'Returns:
' N/A
'
'Owner:
' John Doe
'
'Date:
' dd-MMM-yyyy
'
'-------------------------------------------------------------------------------
ChildObjects("Password").Set GlobalDictionary("Password")
'-------------------------------------------------------------------------------
End Function
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
Public Function Submit()
'-------------------------------------------------------------------------------
'Function: Submit
'Presses the Submit button
'
'Dependencies:
' N/A
'
'Remarks:
' N/A
'
'Arguments:
' N/A
'
'Returns:
' N/A
'
'Owner:
' John Doe
'
'Date:
' dd-MMM-yyyy
'
'-------------------------------------------------------------------------------
ChildObjects("Submit").Click
'TODO: Verify data submission performed successfully
'-------------------------------------------------------------------------------
End Function
'-------------------------------------------------------------------------------
Notice the use of the GlobalDictionary to retrieve the values required by the functions, and the use of the ChildObjects property to retrieve through the test object a reference to the runtime object.
The next step would be to move on to the Business Layer, which implements an application scenario building on the strong foundations of the GUI Layer. For instance to perform the Login function based on the above example, we could wrap it with the following function:
'-------------------------------------------------------------------------------
Public Function do_login()
'-------------------------------------------------------------------------------
'Function: do_login
'Implements the business logic of the do_login Action.
'
'Remarks:
'
'Arguments:
' None
'
'Returns:
' Status
'
'Owner:
' John Doe
'
'Date:
' dd-MMM-yyyy
'
'-------------------------------------------------------------------------------
Dim intStatus, objLogin
Set objLogin = CreateLogin()
If objLogin.Init() Then
objLogin.SetUsername()
objLogin.SetPassword()
objLogin.Submit()
'If login succeeds
intStatus = micPass
Else
intStatus = micFail
End If
do_login = intStatus
'-------------------------------------------------------------------------------
End Function
'-------------------------------------------------------------------------------
Notice the use the Login class outlined above and of its Init function as a precaution to ensure the GUI context is loaded, and not get stuck, as mentioned above. As you can also see, the code within the function above is easy to understand, and is not cluttered with the references to both the OR test objects and data sources as quite often is the case. If changes are made to the GUI objects of a given context, then the changes will be concentrated within a single package, both with respect to the object properties and to the functionality required to manipulate the context’s child objects. Yet another gain from this method is standardization. By implementing code this way we achieve a high degree of homogeneity in the code written by different developers, and thus enhancing the manageability of the automation project.
An advanced alternative to the last example is to pack such a Business Layer function using the Command Wrapper Design Pattern, as outlined in my article Function Pointers in VB Script (revised). For example:
'VB Script Document
Option Explicit
'-------------------------------------------------------------------------------
Class do_login
'-------------------------------------------------------------------------------
'Class: do_login
'Encapsulates the do_login Action.
'
'Remarks:
'
'Owner:
' John Doe
'
'Date:
' dd-MMM-yyyy
'
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
'Methods
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
Public Default Function Run()
'-------------------------------------------------------------------------------
'Function: Run
'Implements the business logic of the do_login Action.
'
'Remarks:
'
'Arguments:
' None
'
'Returns:
' Status
'
'Owner:
' John Doe
'
'Date:
' dd-MMM-yyyy
'
'-------------------------------------------------------------------------------
Dim intStatus
Set objLogin = CreateLogin()
If objLogin.Init() Then
objLogin.SetUsername()
objLogin.SetPassword()
objLogin.Submit()
'If login succeeds
intStatus = micPass
Else
intStatus = micFail
End If
Run = intStatus
'-------------------------------------------------------------------------------
End Function
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
End Class
'-------------------------------------------------------------------------------
Adopting this method would enable the implementation of an advanced generic controller that loads its flow from an external data source such as an XML file. Such a controller device was developed by me together with my partners at SOLMAR Knowledge Networks as part of our generic Object Oriented comprehensive automation framework – My System.
Summary
This article has reviewed an alternative approach to code implementation in a testing automation project that is based on object-oriented principles. I have shown that automation scripting should not be treated differently than software development. Moreover, I tried to convey that logically following this line of thought leads to the conclusion that an automation project must be approached as a software development project par excellence, and suggested an expansion to the OR concept – which was shown as inadequate viz-a-viz the OO paradigm – to encapsulate the interfaces to the GUI objects: the GUI Layer. The article has provided a practical example of how to implement such a layer and how to invoke it using a Business Layer, and explained in detail the benefits of adopting this approach to obtain the maximum ROI from an automation project regarding maintainability, readability, scalability, extensibility and testability. Future articles will expand on this theme and guide the readers into how to gain from the implementation of other design patterns within the framework laid out in this article.


