Object Dictionaries for Generic Functions

Article Tools

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.

Full Function Encapsulating

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.

Previous postIntroduction to Code Design Next postNegative Tests

Related Posts

Post Your Comment

You must be logged in to post a comment.