April 26, 2009

How to use classes in QTP revisited

Classes in QTP are a bit tricky if you want to use them for the first time. You have to know and understand the principle that a class got local scope for the QTP Function Library (.qfl file) it is in, and that if you want to use it in another library, you’ll have to create a constructor (I have shown this before, but I’ll do it again):


' Constructor:
Public Function [new CustomClass]()
     Set [new CustomClass] = new cls_CustomClass
End Function

' Class definition:
Class cls_CustomClass
     Private Sub Class_Initialize()
         Print "Custom Class: I am created!"
     End Sub
End Class

' using the object in another library:
Dim myClass
Set myClass = [new CustomClass]

This seems like a drawback, but it creates a great possibility. Normally, it is not possible to pass initialisation parameters into a new class, because Class_Initialize does not accept parameters. But using the constructor function, we can!

Option Explicit

' Make a private variable that we can pass into the class
Private PARAMETER_ARRAY

' Create a function that returns the requested class. Accepts
' a parameter array. Note: Do not use paramArray, it is reserved!
Public Function [new customClass](ByVal parameterArray)

     PARAMETER_ARRAY = parameterArray

     ' Return a clsCustomClass object
     Set [new CustomClass] = new cls_CustomClass

End Function

' Create the custom class. Classes are always declared Private in QTP
Class cls_CustomClass

     ' The sub Class_Initialize is used to initialize the object with the
     ' parameters passed into the constructor
     Private Sub Class_Initialize()
         ' Do something with the parameters
         Print join(PARAMETER_ARRAY, vbNewLine)
     End Sub

End Class

' Test code
Dim myClass
Set myClass = [new customClass](array("first parameter", "second parameter", "etc."))

Notice what we just did: We created a Private variable, that can only be used by CustomClass classes. So with the use of Private variables with local scope to the library where you put the class in, you create a static variable for the class!
A static variable in this context is a variable that keeps its value and can be used over multiple objects of the same class:

' Declaring the static variables
Private INSTANCE_COUNTER : INSTANCE_COUNTER = 0

' Constructor:
Public Function [new CustomClass]()
     Set [new CustomClass] = new cls_CustomClass
End Function

' Class definition:
Class cls_CustomClass
     Private Sub Class_Initialize()
         Print "I am created and I have " & INSTANCE_COUNTER & " sister(s)."
         INSTANCE_COUNTER = INSTANCE_COUNTER + 1
     End Sub

     Private Sub Class_Terminate()
         INSTANCE_COUNTER = INSTANCE_COUNTER – 1
     End Sub
End Class

' using the object in another library:
Dim a, b, c
Set a = [new CustomClass]
Set b = [new CustomClass]
Set c = [new CustomClass]

Results in:
I am created and I have 0 sister(s).
I am created and I have 1 sister(s).
I am created and I have 2 sister(s).

By using the static variable as a reference for the object itself, we can create a singleton object. A singleton is an object whereof only one instance at a time can exist. In object oriented languages, singletons are normally used for large objects, or objects that will go bad if multiple instances exists like deadlocks or instability.

Option explicit

Private SINGLETON : Set SINGLETON = Nothing

Public function [new Singleton]()
     If SINGLETON Is Nothing Then
         set SINGLETON = new cls_Singleton
     End If

     Set [new Singleton] = SINGLETON
End Function

Class cls_Singleton

     Private Sub Class_Initialize
         Print "I am unique!"
     End Sub

End Class

Dim st1, st2, st3
Set st1 = [new Singleton]
Set st2 = [new Singleton]
Set st3 = [new Singleton]

Results in only one:
I am unique!

There is a drawback: Once a singleton object is created this way, it is not possible to destroy it without the use of some code violating the object oriented principle.

April 24, 2009

QTP variable name conventions

When you start with a new test automation project, your code is conveniently arranged and you still have a clear overview over the locations and naming of variables and functions. But later on it will become a disaster if you don’t manage it a little bit.
That is why I wrote a name convention article. It is not an official how-you-should-do-it document, it is just the way I do things and written from experience.

' Public and Private constants in capitals
Public Const MOUSEEVENTTF _MOVE = 1
Private Const APPLICATION_MAIN_WINDOW = "name:=My Application"

' Functions, Subs and local variables in lowerCamelCase
Public Function myCustomFunction(thisVariable, thatVariable)

     ' Variables with a known type can be declared with the type abbreviation in front of it
     Dim arrStaticArry(5), objFile, intCounter, blnReadOnly

     ' Variables with a unknown type are declared without a type abbreviation, This is also applicable for variables used in self commentary code.
     Dim customContainer, fileWasFound

     ' Consts with local scope are declared with the same format as variables
     Const cannotBeChanged = True

End Function

' Classes in UpperCamelCase. In QTP I use the cls_ tag in front of it for reasons explained later.
Class cls_EventListener

     ' Public variables are in UpperCamelCase too
     Public BufferLength

     ' Private variables with class scope are lowerCamelCase with an underscore behind it
     Private updateCounter_, eventNumber_

     ' Properties, Subs and Functions (public and private) are all in UpperCamelCase
     Public Property Get EventNumber()
         EventNumber = eventNumber_
     End Property
End Class

In QTP, a class gets a local scope. To make it global, you have to add a function returning that class. As a side effect, this gives you the opportunity to initialize the class. Of course you have to add an init method to your class if you do it this way.

Public Function [new EventListener](initializationParameters)
     Set [new EventListener] = new cls_EventListener
     [new EventListener].Init(initializationParameters)
End Function

Now, you can set a new class with the following code in another library:
Set EventListener = [new EventListener]("codebase:=Unicode")

Side note: The square brackets around a variable lets you enter every character for a variable, including spaces, special characters etc. If you find that inconvenient, you can use an underscore: new_EventListener to mimic normal VB functionality.
I use the square brackets when I want to express importance for example:

' Call the main routine of this script:
[___ !MAIN! ___]

' Or to enjoy my co workers (and to see if they ever peer review my code):
Dim [ O\-<>-/O ], [ ¿Que? ]

April 8, 2009

A few ways to use arrays

When you first encounter Arrays in QTP it is not very easy to understand quickly. VBScript makes use of a few types of arrays: Static, Dynamic, Assigned to a normal variable and Dictionary objects (the dictionary object is not discussed in this article.). Static and Dynamic arrays can be one dimensional or multidimensional.

First the simple static array. Static because it can only contain a fixed amount of items; Simple because we only put strings in it referred by the indexnumber (or subscript) of the array:


' Simple static array
Dim weekdays(6)

weekdays(0) = "Mon"
weekdays(1) = "Tue"
weekdays(2) = "Wed"
weekdays(3) = "Thu"
weekdays(4) = "Fri"
weekdays(5) = "Sat"
weekdays(6) = "Sun"

When you don't have plans to use the subscript, you can also create the array directly. And yes, it works the same with index numbers as with the weekdays array, but it is good programming practice to use the former method if you want to call the array items by subscript number.

' Simple assigned array
Dim workdays
workdays = array("Mon", "Wed", "Thu", "Fri")

As you can see, the variable does not have to be declared as an array, so theoretically every variable can be set as an array just as every variable can be set as an object. Although, with arrays you don't need the set statement.

Sometimes it is more convenient to dynamically increase and decrease the array size. Then you have to create a dynamic array. This is an array declared the same way as in the simple static array, but without the number of elements, just empty parenthesis: dim myArray()
To set the amount of array items, you have to use ReDim like ReDim myArray(n) where n is the amount of items you want to use +1 (The first subscript is always 0). Other then in a static array declaration (like Dim myArray(5)), the number of subscripts in a ReDim statement can be a variable or constant.
Whenever you use ReDim, the array is reïnitialized, except when you use the Preserve command, indicating you want to reuse the already set values:

' Simple dynamic assigment of an array
Dim myPets()
ReDim myPets(2)
myPets(0) = "Dog"
myPets(1) = "Cat"
myPets(2) = "Hippopotamus"

ReDim Preserve myPets(3)
myPets(3) = "Rabbit"

msgbox "My pets: " & join(myPets, ", ")

The join(array[, separation character(s)]) function lets you easily merge an array to a string.
Another trick is to use join to make an html table easily:
newTableRow = "" & join(arrElements, "") & ""

Multidimensional arrays
A multidimensional array is used to store data or objects in a matrix. You can create virtually create as much dimensions if you want (not really unlimited of course, but keep in mind this good rule with programming: If you have to ask what the limit is for some kind of instance, probably there is something wrong with your design)


' Multidimensional static array, the next code is pure and alone for demonstration purposes
' it is not optimised, maybe even not correct and there are better ways to achieve this
Dim bcCalendar(2100, 12, 31)
Dim dayCounter, maxDay, cYear, cMonth, cDay
dayCounter = 6

For cYear = 0 to 2100
    For cMonth = 1 to 12
        Select Case cMonth
            Case 1,3,5,7,8,10,12    maxDay = 31
            Case 4,6,9,11           maxDay = 30
            Case 2                  maxDay = 28 + abs((cYear mod 400 = 0) or ((cYear mod 4 = 0) and not (cYear mod 100 = 0)))
        End Select

        For cDay = 1 to maxDay
            bcCalendar(cYear, cMonth, cDay) = weekday((dayCounter mod 7)+1)
            dayCounter = dayCounter + 1
        Next
    Next
Next

msgbox "Charles Darwin was born on a " & WeekdayName(bcCalendar(1809, 2, 12))



Passing arrays
Arrays are passed the same way as variables are:

Default: By Reference
With ByRef in the function declaration: By Reference
With ByVal in the function declaration: By Value
With parenthesis around the argument in the function call: always By Value (ByRef in the function declaration is omitted)

Arrays filled with objects
Arrays do not only have to contain variables, they can also contain objects, which is great to make a collection of QTP gui objects, dictionaries, but also class objects to create child classes under a parent.
Arrays can even contain other arrays. This is useful if you want to create a fully dynamic multidimensional array. (Normally, in a multidimensional array, only the last item is expandable with a redim preserve statement.)

December 10, 2008

Performance increase in table lookup functions

Using object properties instead of QTP standard functions will improve the performance of QTP tests significantly. In our case, we often want to lookup the location of a certain value in a WebTable. QTP provides several functions to read out data from a table, but is slow when used iterating the table (like two for loops, one iterating the rows, the embedded second one iterating the columns per row).

Example of a conservative way to do this:

Public Function LocateTextInTable(byRef tbl, textToFind, byRef row, byRef col)

    For row = 1 to tbl.RowCount
        For col = 1 to tbl.ColCount
            If tbl.GetCellData(row, col) = textToFind then
                LocateTextInTable = True
                Exit function
            End if
        Next
    Next

    row = -1 : col = -1
    LocateTextInTable = False
End Function

The crux is this: .GetCellData is not a very fast method and with a table, consisting of 30 rows and 10 columns, this method is iterated up to 300 times in the most worse case scenario (= text not found).

A faster way to retrieve the data is through the Document Object Model (DOM). This allows you to use the more native properties of an object with the sacrifice of some ease of use.

A table consists of row elements and each row element contains one or more cells. We can iterate them just the same way as we did with the function above:

Public Function LocateTextInTableDOM(byRef tbl, textToFind, byRef row, byRef col)

    Dim objRow, objCell

    row = 1 : col = 1

    For each objRow in tbl.object.Rows
        For each objCol in objRow.Cells
            If objCol.Value = textToFind then
                LocateTextInTableDOM = True
                Exit function
            End if
            col = col + 1
        Next
        row = row + 1
    Next

    row = -1 : col = -1
    LocateTextInTableDOM = False
End Function

From our experience, this will increase the performance of the function with a factor 10.
But be aware, there is one big assumption: This function assumes that the row objects (and cell objects) are perfectly iterated from the first row to the last row and in this exact order. Although a For…Each construct cannot guarantee this behaviour, we never encountered an incorrect location.

November 4, 2008

How to make use of Function Pointers in a Keyword Driven Approach


A commonly used automated test methodology is working with user defined keywords. The tester defines the keyword with parameters next to it and the automated testtool processes this keyword and executes the corresponding function.

There are two ways I know of to make this work in QTP, both with advantages and drawbacks.
A "select case" construction gives you flexibility, but with every keyword you have to add a case manually.

Select Case KeyWord
   Case "login" call login()
   Case "enter user" call enterUser()
   Case Else msgbox "Invalid Keyword: '" & KeyWord & "'"
End Select

Using an eval or execute can lead to type mismatch errors. You can circumvent this by putting in an "On error resume next", but this will lead to no error handling at all by the VBScript engine. Any undefined variable normally results in a "Variable is undefined" error, which can be a great help. You are losing that with an "on error resume next" statement.
Also, a execute or eval does not return a value or object, unless you are capturing that with a variable in the string of the execute itself.

On error resume next
Execute ("TestResult = " & KeyWord & "()") ' -> Any error within the processing of the KeyWord will not be trapped.
On error goto 0

In this post, I will show you another way to dynamically assign keywords to functions in a fast way, with trapping of invalid keywords on a soft way (programmatically and not by the VBScript engine), and keeping the normal error behaviour. This can be done by creating a function pointer to the function and verifying if the function pointer is valid. If so, it can be executed as if it is a normal function:

Dim fp

On error resume next
Set fp = GetRef(KeyWord)
On error goto 0

If IsObject(fp) then
   TestResult = fp
Else
   MsgBox "Keyword '" & KeyWord & "' could NOT be mapped to a valid function."
End if


A function pointer can be used just as a normal function. A drawback of VBScript is the use of fixed arguments, but this can be solved as found elsewhere on teh internets: Passing an array or a dictionary object to the function.

Dim aryArguments
Call GetArguments(aryArguments) 'A function to set all arguments into an array
TestResult = fp(aryArguments) 'Pass the arguments to the function pointed to by the function pointer


And a quick example:

Public Function SumSquares(byRef aryArg)
  Dim i, b
  For i = lbound(anyArg) to ubound (aryArg)
     b = b + aryArg(i)^2
  Next i
  SumSquares = b
End Function

Public Function SqrtSquares(byRef aryArg)
  Dim i, b
  For i = lbound(anyArg) to ubound (aryArg)
     b = b + aryArg(i)^2
  Next i
  If b >= 0 Then SqrtSquares = Sqr(b)
End Function

Public Function HandleKeyword(byval keyWord, byRef aryArg)
  Dim fp

  On error resume next
  Set fp = GetRef(keyWord)
  On error goto 0

  If IsObject(fp) then
     HandleKeyword = fp(aryArg)
  Else
     MsgBox "Keyword '" & KeyWord & "' could NOT be mapped to a valid function."
   HandleKeyword = "INVALID KEYWORD"
  End if
End Function

Dim Arguments
Arguments = array(3, 4, 5)

MsgBox HandleKeyword("SumSquares", Arguments) ' -> shows "50"
MsgBox HandleKeyword("SqrtSquares", Arguments) ' -> shows ~ "7.07107"
MsgBox HandleKeyword("SumFactoerials", Arguments) ' -> shows "INVALID KEYWORD"

October 14, 2008

Parenthesis DO matter

One thing I never knew: Parenthesis matter in QTP (and VBScript). And they surely make a difference. I discovered this during debugging a function call. There was different behaviour between these two function calls:
foo(bar)
call foo(bar)

After some research over the internet and in QTP self, I came to the following conclusions: Passing an argument to a function surrounded by parenthesis means: "Protect me" or in other words: treat me as byVal even if it is defined in the function as byRef.

Example:
sub samplesub (a, b) ' a and b are by default handled as byRef
    a = a + b
end sub

And this is happening when we call samplesub:
x = 1
y = 2
z = 4
samplesub x, y
samplesub (z), y
msgbox x ' displays "3"
msgbox z ' displays "4" because z was passed as if it was passed byVal

The same applies when you call a function:

function samplefunc(c)
    c = c^2-1
    samplefunc = (c mod 2 = 1)
end function

q = 8
samplefunc q
msgbox q ' returns 63

' When you accidentally forgot to call:
p = 9
samplefunc(p)
msgbox p ' returns 9, because p is returned byVal

' With call:
r = 10
call samplefunc(r)
msgbox r ' returns 99, because r is returned byRef

' With call and argument protected:
s = 11
call samplefunc( (s) )
msgbox s ' returns 11, s is returned byVal

' And a last example of a function call with multiple argument with combined protection:
call multifunc( (IamProtected), IamUnprotected )

Rules in short:
A sub/function call with an argument in protected mode overrides a byRef in the function.
A sub/function call with an argument in unprotected mode is returned byRef by default unless it is overridden in the function by a byVal.
An literal or const is always returned byVal.

Syntax proposal:
OK, it is ugly, but if you use parenthesis because they are part of the call, you should use them with spaces between the first and last argument and no space between the function:

call f( a, b )

If you want to use arguments in protected mode, you should use no spaces between the parenthesis and the arguments, but do use them between the function/sub and the parenthesis belonging to the function/sub call:

f (a), (b)
or
call f( (a), (b) )

July 7, 2008

QTP Quick Reference Card



As a WinRunner user, I've got very used to the concept of automated testing. Changing to QTP was not much of a problem except for the language notation. For example, the thing I always do wrong:




Select Case anyVariable
    Case 1,2
        ...
    Case Default ... ***CRASH***

And you'll only discover it at runtime, because Default is displayed in blue as it is used to set the default property for a class. ('Default' is the 'Case Else' in C/C++ or TSL).

For all advanced users having trouble with VBScript notation on the expert view, I want to share my QTP Quick Reference Card (also called Cheat Sheet). It displays all general functions you'll want to use in QTP.

The PNG version:



Or the smaller and more portable PDF version:



If you have any comments or additions on this document, please leave a message in the comments.