Object Dictionaries for Generic Functions
Posted by admin - Mar 29, 2008 Code Techniques, Yaron Assa 0 1 Views : 541 Receive Updates For This Category
Article Tools
- Print this page
- Add Comment
- Send to Friend
- Last Updated on :
Jul 15, 2011
Today we’ll cover a valuable technique that will enable us to greatly simplify and organize our code, by adding meta-data to our QTP objects. The technique is best used when coupled with DP, though with certain adjustments, it works fine with OR based scripts as well.
This article relies heavily on DP, dictionary objects, and to some extent, code-design terms. So make sure you’re up-to-date on the relevant subjects.
The Problem
In the real world, applications are never truly 100% generic. There will always be that one patch, field type or input validation, that doesn’t fit into a logical, consistent description of the system. This is why our scripts can never be 100% generic either, and would always have to relay on hard-coded exceptions at certain points.
So, for example, when writing an InputField function, we try our best to find some rule that will enable us to automatically know how to input each and every field in the application. Usually we can indeed find such a rule (e.g. by the class of the field’s object), and come up with an elegant, generic structure:
Function InputField(oFieldObject, sValue)
Select Case oFieldObject.GetTOPRoperty("micclass")
Case "VBEdit"
oFieldObject.Click
oFieldObject.Type sValue
Case "VBCheckbox"
oFieldObject.Set sValue
Case "ActiveX"
oFieldObject.Object.Value = sValue
End Select
End Function
However, it won’t be long before an exception to the rule will emerge, and will force us to break the generic structure with custom patches. Since these exceptions tend to pile up, pretty soon we end up with something like this:
Function InputField(oFieldObject, sFieldName, sValue)
'Can only except up to 10 chars
If sFieldName = "A" Then sValue = mid(sValue, 1, 10)
Select Case oFieldObject.GetTOPRoperty("micclass")
Case "VBEdit"
oFieldObject.Click
oFieldObject.Type sValue
Case "VBCheckbox"
oFieldObject.Set sValue
Case "ActiveX"
If sFieldName = "B" then
Set oChildren = oFieldObject.ChildObjects(oDesc)
oChildren(1).Type sValue
Else
If sFieldName = "C" then
oFieldObject.Object.Value = sValue
oFieldObject.Type MicReturn
Else
oFieldObject.Object.Value = sValue
oFieldObject.Type MicTab
End If
End if
End Select
End Function
If this wasn’t bad enough, similarly complex structures will emerge in other functions (e.g. EditField, DeleteField, and SaveEntity), or any function for that matter. Even if your application doesn’t have input-field functionality, I’ll bet my life that it has some functionality that suffers from a similar problem. Pretty soon the whole structure becomes so complex and entangled, that just thinking about adding another field to the party becomes frightening.
The reason this problem is so acute and common is that it’s based on a problem with the code’s conceptual design. In order to know how to work with the different objects in the application, our scripts need more data than how to merely identify them. Since we can’t attach custom data to the QTP objects themselves, we’re forced to put it somewhere else – usually within our functions, and this makes them bloated and ugly.
To put it in code design terms, we’ve deeply violated the encapsulation and the Separation of Concepts principles. Our objects should be self-reliant, and contain within themselves (encapsulated) any data needed to use them (e.g. what method should be used for inputting values into them). From the function’s side, we’ve entangled the function’s “factory” (what performs the task), with the “manufacturing instructions” (the data to perform these tasks). We’ve mixed the different concerns instead of separating them.
This would be OK if there was only one standard way of doing the function’s tasks, but as we saw, almost every object has a different way of doing the same task. Different ways = complex, ugly, unreadable, and hard to maintain functions.
In order to re-achieve encapsulation and SoC, we must shift our point of view. We must understand that identifying an object in the real world is just one of many tasks we must perform with it. And just as each QTP object holds within itself all the data needed to identify it, so it must hold within itself all the data needed to perform any other task. We shall now see how this could be achieved.
The Solution
Our data structure will be extended to represent the new point of view: each object will be a data-dictionary, with many different values. One of the values will be the QTP identification reference. The other values will contain any data needed to operate the object. So we’ll get something like this:
Object:
Reference = The QTP object
InputMethod = “Standard” / “Value Set” / “Object Set”
ExitMethod = “Standard” / MicTab / MicReturn
Well, you get the picture. The point is that now our object holds clear instructions for our functions to execute. There’s a perfect Separation of Concerns – our functions can return to be simple factories, that receive manufacturing instructions and execute them; and our objects are responsible for the data and descriptions that compose these instructions.
If an object were to change, we wouldn’t need to touch our delicate functions, but only the way the object is described. Similarly, a new object in our application will not change you “factories”, but only their manufacturing instructions.
Implementing the Solution
So, how is this actually done? Shockingly enough, objects dictionaries are implemented using dictionary objects. The best way for doing this is using an “object genie” function (abstract idea first presented here), which returns the needed dictionary:
Function GetObjectDic(sObjectName)
Dim oResult
Dim oQTPRef
Dim sInputMethod
Dim sExitMethod
Set oResult = CreateObject("Scripting.Dictionary")
Select Case sObjectName
Case "A"
Set oQTPReft = VBWindow("vbname:=x").VBEdit("vbname:=y")
sInputMethod = "Standard"
sExitMethod = "MicReturn"
Case "B"
Set oQTPReft = VBWindow("vbname:=x").VBCheckBox("vbname:=w")
sInputMethod = "QTP Set"
sExitMethod = "MicTab"
Case "C"
Set oQTPReft = VBWindow("vbname:=x").ActiveX("acx_name:=y")
sInputMethod = "Object Set"
sExitMethod = ""
End Select
oResult.Add "Ref", oQTPReft
oResult.Add "InputMethod ", sInputMethod
oResult.Add "ExitMethod", sExitMethod
Set GetObjectDic = oResult
End Function
And now our function can get back to be generic and elegant (as possible):
Function InputField(oFieldObjectDic, sValue)
'Input
Select case oFieldObjectDic("InputMethod")
Case "QTP Set"
oFieldObjectDic("Ref").Set sValue
Case "Object Set"
oFieldObjectDic("Ref").Object.Value = sValue
Case Else 'Standard
oFieldObjectDic("Ref").Type sValue
End Select
'Exit
oFieldObjectDic("Ref").Type oFieldObjectDic("ExitMethod")
End Function
As you can see, even in the very unlikely case a new field will require changes in the functions themselves (because it needs an entirely new method for exiting), there’s no real problem. The functions are so simple and clear, that adding a new method segment is very easy and isolated from the rest of the code.
Further Tweaking
What we presented here is only the tip of the iceberg. The dictionary structure is extendible, so it’s very easy to add data for many other functions. Just as an example, sValueType = {Text/Number/Check/Date} can be easily used to create random values for each field.
There are two extensions that are worth mentioning specifically: identification delegation and full function encapsulating. Other then these two, I will elaborate on using classes instead of dictionary objects, in a separate article.
Identification Delegation
In the above example we’ve dealt with an extremely simple case – one window with three objects. In real life, there are dozens of objects in many windows. This can make our Object Genie function messy and cluttered. This can be resolved with identification delegation:
'sObject name = WindowName>ObjectName
Function GetObjectDic(sObjectName)
Dim oResult
Dim arrName
arrName = split(sObjectName, ">")
Select case arrName(0) 'The window Name
Case "Main"
'Delegate identification to sub-function
Set oResult = GetObjectDic_Main(arrName(1))
Case "Files"
'Delegate identification to sub-function
Set oResult = GetObjectDic_Files(arrName(1))
'More Cases
End Select
Set GetObjectDic = oResult
End Function
Function GetObjectDic_Main(sObjectName)
Dim oResult
Dim oWindow
Dim oQTPRef
'All the QTP references will be under the same window, so lets put it centrally
Set oWindow = VBWindow("vbname:=Main")
Set oResult = CreateObject("Scripting.Dictionary")
Select case sObjectName
Case "A"
Set oQTPRef = oWindow.VBButton("vbname:=A")
'A lot of data and values
Case "b"
'More code
'More Cases Here
End Select
'Return delegated answer to the master function
Set GetObjectDic_Main = oResult
End Function
'More sub-Functions as needed
This way our functions remain tidy and relatively small, and when a new object will be introduced, we will know exactly what piece of code will need updating.
As you probably noticed, even with the new design, we haven’t achieved full encapsulation. Some of our “business logic” remained in the functions, instead of being entirely encapsulated within our objects dictionaries.
The following solution solves this problem, but with a heavy cost. When using full function encapsulation, our code becomes more complex, and hard to read and maintain. It’s left for each of you to weigh the trade-off, and to decide if it’s best to stop here, or to take the extra step.
The whole concept lies on putting the actual code needed to perform certain tasks, within the object. So, the ExitMethod value for the object now becomes “oFieldObjectDic(“”Ref””).Type MicTab”. And within the InputField function we get the command: Execute oFieldObjectDic(“ExitMethod”)
As you can see, there’s absolutely no select-case of if structure. The functions remain at the pure command-executing domain, and the separate objects now truly contain all the data needed to operate them. They’ve become completely self-reliant.
This, however, brings a new problem to the table. Assuming most of our objects need the run-of-the-mill, standard operation performed on them, we’ve created massive amounts of duplicate code. Most of the object will contain the same strings of code over and over again, and this will become a major issue for future maintenance and updates.
The best way to deal with this is to create a new function called StandardX, where X is the relevant functionality (e.g. StandardExit). Now all the objects that need the run-of-the-mill, standard operations, will include “StandardX(oFieldObjectDic(“”Ref””))” as their dictionary value. Any needed change or maintenance will be done once, on the “Standard” function, and this will automatically apply to any object pointing at it.
As you can see, the whole operation becomes more complex and hard to track. It does successfully shift the complexity from the execution functions, to the separate objects in the application, and it makes long-term maintenances and upgrades less risky, but it’s not at all clear that the downsides are worth it. The decision is left to you.
Summary
To sum up, function patches and entanglement is a common illness, which can have dire consequences on future maintenance and code updates. This article covered one of the more successful techniques (in my experience) for dealing with these problems. The technique suites DP perfectly, since you can easily fit the object-identification into the dictionary building functions, but it can work just as well with OR based scripts.
As always, the example code snippets are the tip of the iceberg. It’s recommended you experience with these methods, and mold them into your own unique coding and scripting style.


