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.

July 1, 2008

Get the text from a tooltip with WinRunner

Getting the text form a standard tooltip in an HTML document with WinRunner took me some time to figure out. Actually it is one small statement that will do the trick, but finding out what statement kept me searching for a while.

win_get_info("{class: window, MSW_class:\"tooltips_class32\",displayed: 1}","text",text);

This will do it. Really.

And to wrap it into a function:

 
public function get_tooltip(object, out text)
{
        auto hwnd, rc;
       
        # First set the cursor position to the left corner of the screen. When you don't do this,
        # a formerly captured tooltip can be hovering over the object, making it impossible
        # for WinRunner to locate it.
        rc = move_locator_abs(1, 1);
       
        # Move the cursor to the object containing the tooltip 
        web_cursor_to_obj(object, 1, 1);
 
        # Move the cursor back and forth to trigger the tooltip to pop up
        move_locator_rel(0,1);
        move_locator_rel(0,-1);
 
        # Wait for the tooltip. This is time consuming, something to keep in mind when
        # you want to use this function
        wait(2);
       
        # Check on visibility of the tooltip window, otherwise a standard tooltip does not exist for this object               
        if((rc = win_exists("{class: window, MSW_class: \"tooltips_class32\",displayed: 1}")) == E_OK)
               
                # Capture the text
                return win_get_info("{class: window, MSW_class: \"tooltips_class32\",displayed: 1}","text",text);
       
        return rc;
}
 

Keep in mind, this only works with standard tooltips, recognizable on the title='[text]' attribute in the HTML tag (not always visible when CSS is used). When the tooltip is a customized tooltip created by some JavaScript, most probably it is created as a frame object and you'll have to capture the text of that frame object in stead.

TestNet Voorjaars Event

Yesterday was the day of the TestNet Voorjaars Event. TestNet is the national Dutch organization for testers. There were some great presentations and off course the warm bath experience of seeing lots of old collegues. That is one advantage of being Dutch: The test community is not very large in numbers. TestNet has around 1200 members, while the test community is as large of 6000 professional testers.

The theme was TestTooling, so I fitted well. Actually, I was invited to do a presentation, which I did with Marc Koper, a collegue of mine.

If you are interested in the presentations (some real good ones included), you can find them at the TestNet Library Page. However, most of them are in Dutch.
If you are especially interested in our presentation, it is called "A tool with a fool is only a tool". It is an introduction in how you can set op test automation (Performance and Test execution) in a test organisation with lots of tips and tricks. Well, for the best tips and tricks, you should have been there, but later this month, I will put some of them on my blog.

Update: The abstract of our presentation

June 28, 2008

This Weekend's Code Play

Right now, I am almost finishing Gödel, Escher, Bach written by Douglas R. Hofstadter. Well, I'm on page 614, so I still have around 200 pages to go, but for me it feels like almost finishing. And this inspired me to a little piece of code in VBScript:




Dim a, p 
Dim YmPool(8)

Do
    mIP = afNe(a+3)*2
    NextElem = el
    
    If YmPool((a+2)*3) = aDig Or p = 1 Then 

        InTest = a
        LitNuod "nametag", "Gateman"

        Do Until a = t
            Set NineHt1 = ProgId
            a = (3*(2+a))
        Loop 

        MyFile = Me
        Let xEn = 2*(3+a)
        
    End If
    
    a = PI Mod (8)
Loop 

myMid p, amId


Do you see the catch?

My next challenge: Write an actual snippet in VBScript this way that makes sense and that is actually usable.

June 25, 2008

QTP: IBAN validation in VBScript

IBAN stands for International Bank Account Number and is the old new toy of the banking community. Also hot in Europe because of SEPA. IBAN should make life easier, and maybe it does. For IT guys, IBAN is just another standard. And despite IT guys like standards (that is why they have so many of them), IBAN is a standard designed by the banking people making things a little more complicated.

The things you want to do with IBAN is validate it or just calculate the checksum on your own. The fo
rmula for the checksum is not very complex, but has some twists in it. For example, when dealing with Alpha characters, the A is transposed to 10, the B to 11 etc. In the IT world, we would transpose A to 65, B to 66… The things you don't want is validate them exactly right for for every country on this little planet. Maybe they want it, but definitely, you don't. And if they want it, get yourself a new toy called SOAP and connect to it through a service.

After searching the internet, I discovered that code for IBAN validation through any Visual Basic language was rare. I gathered the snippets I found useful and created my own IBAN functions.

How it works is all in the comments in the code, keeping your scripts maintainable and documented if you want to use it:

' This code was created by Bas M. Dam and first published on
' http://automated-chaos.blogspot.com
' You can use and distribute this code freely, as long as you
' keep this commentblock intact.

' RETRIEVING THE CHECKSUM
' There are two methods to get the checksum. The first is the
' one used in automated processes where there is an iban prototype.
' the checksum is replaced by zeros:
' MsgBox getIBANchecksum("LC00BANK1234567890", empty) 'returns 86

' The other way is a more user fiendly appraoch if only the separate
' segments are known like bank code or clearing house:
' MsgBox getIBANchecksum("BANK1234567890", "LC") 'returns 86

' CREATE AN IBAN NUMBER
' This is implemented in the makeIBAN() function for your convenience
' Msgbox makeIBAN("LC", "BANK", empty, "1234567890")    
    ' returns LC86BANK1234567890

' Or just the simple implementation:
' Msgbox makeIBAN("LCBANK1234567890", empty, empty, empty)
    ' returns LC86BANK1234567890

' CHECK AN IBAN NUMBER
' And finally, you want to check if something is IBAN. You can
' use the getIBANchecksum function for it. If the result is 97,
' then you have a real IBAN, when it returns -1, there is something
' wrong with the IBAN and if it returns another number, the checksum
' is not correct
' Msgbox getIBANchecksum("LC86BANK1234567890", empty) 'returns 97
' Msgbox getIBANchecksum("LC68BANK1234567890", empty) 'returns 18
' Msgbox getIBANchecksum("LC68BANK1234567891", empty) 'returns 88
' Msgbox getIBANchecksum("LC86BANK123456789%", empty) 'returns -1

' To do this the simple way, you can make use of the isIBAN() function
' that simply returns True or False:
' Msgbox isIBAN("LC86BANK1234567890") 'returns True
' Msgbox isIBAN("LC68BANK1234567890") 'returns False
' Msgbox isIBAN("LC86BANK123456789%") 'returns False


' SPECIAL CHARACTERS
' You can use typographical characters as stated in the skipChars string.
' For now, the following characters can be used: space.-_,/
' These characters are often used to make an IBAN more readible, but are
' not taken into the checksum calculation. between the landcode
' and checksum, never a typographical character can be used.
' Msgbox isIBAN("LC86 BANK 1234 5678 90") 'returns True
' Msgbox isIBAN("LC86BANK1234.56.78.90")  'returns True
' Msgbox isIBAN("LC-86-BANK-1234-567890") 'returns False, there can not
                    'be a separation char between
                    'landcode and checksum.
' Msgbox isIBAN("LC*86*BANK*1234*567890") 'returns False, * is not a special char

' Function to check on an IBAN
Public Function isIBAN(sIban)

  isIBAN = (getIBANchecksum(sIban, empty) = 97)
   
End Function

' Function to create an IBAN. Any of the arguments can be empty, as
' long as the first not empty argument starts with the landcode
Public function makeIBAN(landcode, bankcode, sortcode, accountnr)

    dim realLandcode, sPurged

    sPurged = mid(landcode & bankcode & sortcode & accountnr, 3)
    realLandcode = left(landcode & bankcode & sortcode & accountnr, 2)
    
    makeIBAN = realLandcode & getIBANchecksum(sPurged, realLandcode) & sPurged
   
End Function

' Function to get an IBAN checksum. Landcode can be empty, but then, the landcode
' must be included in the first two characters of sIban, followed by two zero's
Public Function getIBANchecksum(sIban, landcode)

  Dim sLCCS        'Land Code and Check Sum
  Dim sIbanMixed
  Dim sIbanDigits
  Dim char
  Dim i
  Dim skipChars
  skipChars = " .-_,/"
    
  ' Marginal length check
  If Len(sIban) < 5 Or Len(sIban) > 35 Then
    getIBANchecksum = -1
    Exit Function
  End If

  If landcode = empty Then
    sLCCS = Left(sIban, 4)     '   Extract land code and check sum
    sIbanMixed = Right(sIban, Len(sIban) - 4) & UCase(sLCCS)
  else
    sLCCS = landcode & "00"
    sIbanMixed = sIban & UCase(sLCCS)
  End If
   
    For i = 1 To Len(sIbanMixed)
        char = Mid(sIbanMixed, i, 1)

        'Check on digits
        If IsNumeric(char) Then         
           sIbanDigits = sIbanDigits & char    
    
        'Check on typographical characters
        elseif instr(skipChars, char) Then  
           'skip this character, but continue
    
        'Check on non-uppercase other characters
        elseif Asc(char) < 65 OR Asc(char) > 90 then
           getIBANchecksum = -1
           Exit function
   
        'Transform characters to digits
        else
           sIbanDigits = sIbanDigits & (Asc(char) - 55)
        End If
    Next

    getIBANchecksum = 98 - largeModulus(sIbanDigits, 97)
      
End Function

' Calculates the modulus of large integers that are actually
' strings. Also usefull for implementation in Excel VBA
' (there is a known bug in Excel and large number modulus)
Private Function largeModulus(sNumber, modulus)
    Dim i, sRebuild(), j, r
    
    j = 0
    sNumber = cStr(sNumber)
    
    For i = 1 To Len(sNumber) + 6 Step 6
        ReDim Preserve sRebuild(j)
        
        sRebuild(j) = Mid(sNumber, i, 6)
        j = j + 1
    Next

    r = sRebuild(0) Mod modulus
    
    For i = 0 To UBound(sRebuild) - 1
        r = (r & sRebuild(i + 1)) Mod modulus
    Next
    
    largeModulus = r
    
End Function

The knowledge about the IBAN validation and some code tricks I retrieved from the internet, so it is my turn to to give it back to the community. The functions are also useful in Excel VBA, but not extensively tested. The isIBAN() function is great to use it in your spreadsheet it
self, or use it as conditional formatting:


June 18, 2008

Continued: Creating a Queue data type in QTP

DONG, Housekeeping message: If you have questions, additional information, an opinion or just something to share, you are very welcome to leave it in the comments. I like to get personal mail too, but as mentioned on coding horrors: on a blog, the comments are the best part. And since I just started, I still need some!

In addition to a question I received through email: The class to create a queue on the linked list I published Monday, and how to perform a count on this queue.

Public function queue()
  set queue = new clsQueue
End Function

Class clsQueue

  Private ll

  Private Sub Class_Initialize  ' Setup Initialize event.
    set ll = linkedList     ' Create a linked list instance
    End Sub

  Public function pop()
    If ll.count = 0 Then exit function  ' No items in the list
        pop = ll.getfirst()     ' Return the last item
    ll.deleteFirst        ' and delete it
  End Function

  Public sub push(element)    
    ll.add element        ' Add an item to the list
    End sub

  Public function peek()      
    peek = ll.getfirst()      ' Peek at the top item
  End Function

  Public property get count()
    'return the amount of nodes
    count = ll.count
  End Property
  
End Class

And this is the part where inheritance and polymorphism is missing. The class looks kind of the same as the stack class, but .getlast() is replaced by .getfirst(). I wish everything in life was this simple… The count property is added, so you can now count the items in the queue. If you want to add a count property to the stack, just do the same in the clsStack.

June 16, 2008

Abstract data types in QTP: The linked list

Normally the need for abstract datatypes in Test Automation is not very high. But you will see, just if you don’t have direct access to them, you’ll need them most. Today I had to store some data and it would be nice if I could pop and push it on some stacks. The basis for stacks (and queues) is a linked list. This was a nice Monday morning starter. Linked lists are simple, if you have the right toolset. Unfortunately, QTP and VBScript do not support pointers, so I had to be a little creative. First of all, I created a node class. Each node has a unique index number and a reference to the index numbers of its left and right neighbours. The data is conserved in a public available data element that can contain objects or variants.

' This is a setup for a linked list. This linked list is the basis for
' queues and stacks. With a little adaptation this can be transformed to
' a binary three or more complex abstract data types.

Option explicit

' Initializer for a new listnode. If you keep it private, you don't need this
' caller function
Private function listNode()
    Set listNode = new clsListNode
End Function

' class for a node. The nodeIndex functions as pointerreference
Class clsListNode

    Public data              'Data, can be a variable or an object (not an array)
    Public prevNode
    Public nextNode
    Private nodeIndex        'Index reference

    ' I use a property get to make a defaulter
    Public default property get index()    
        index = nodeIndex    
    End Property

    ' and a property let to set a new indexer
    Public property let index(inr)
        nodeIndex = inr
    End Property
End class


To keep track of the nodes, I created an array called collection containing node elements. Collection() is a list of references, but is never referred directly except when we add a new node. If a new node is added, the subscript of collection is incremented to create a new unique reference. Because elements can be added or deleted randomly, do not use collection() and its subscript as a continuous or a chronological list!

With add(), we can add an element to the end of the linked list. With getlast() and getfirst() the last and first data elements on the linked list are returned and with deletelast() and deletefirst() the last and first nodes are deleted.

Count() is a helper function, returning the amount of nodes in the list.

' Make the linked list caller
Public function linkedList()
    set linkedList = new clsLinkedList
End Function

' The linked list call
Class clsLinkedList

    private    collection()       ' a single dimensional array of listNodes
    Private lastItemIndex         ' index of the last item
    Private firstItemIndex        ' index of the first item
    Private indexNumber           ' counter for nodes in the list. Only increment, never decrement!
    Private nodeCount             ' amount of nodes. Ugly but fast.
  
    Private Sub Class_Initialize  ' Setup Initialize event.
        lastItemIndex = null   
        firstItemIndex = null
        indexNumber = 0
        nodeCount = 0
    End Sub

    Public sub add(data)

        ' add new listNode element to the array
        ReDim preserve collection(indexNumber)
        set collection(indexNumber) = listNode

        With collection(indexNumber)
            ' Make the collection compatible for objects as well as normal variables
            If isObject(data) Then
                set .data = data
            else
                .data = data
            End If

            .prevNode = lastItemIndex    ' The previous node of the new node is the current
                                         ' last node reference
            .nextNode = null             ' As it is the last item, there is no reference
                                         ' to the next item
            .index() = indexNumber       ' Set the index of the node to a unique number

            If not isnull(.prevNode) Then     ' If the newly created node has a left neighbour:
                collection(.prevNode).nextNode = indexNumber    ' the nextnode reference of the left
            else                                                ' neighbour is the index of the newly created
                firstItemIndex = .index       ' Else, this is the first node and the first node reference is the
            end if                            ' index of the newly created node
        end with

        lastItemIndex = indexNumber          ' As this is the last node, set the reference to this indexnumber
        indexNumber = indexNumber + 1        ' make a new indexnumber for a unique reference next time
        nodeCount = nodeCount + 1            ' and increment the nodecounter
    End sub

    Public property get getlast()
        If nodeCount = 0 Then exit property                ' No nodes? exit!

        If isobject(collection(lastItemIndex).data) then   ' Object or variant?
            Set getlast = collection(lastItemIndex).data   ' return object
        else
            getlast = collection(lastItemIndex).data       ' return variant
        end if   
    End Property

    Public property get getfirst()

        If nodeCount = 0 Then exit property

        If isobject(collection(firstItemIndex).data) then
            Set getfirst = collection(firstItemIndex).data
        else
            getfirst = collection(firstItemIndex).data
        end if
   
    End Property

    Public sub deletelast()

        If nodeCount = 0 Then exit sub    'Exit on no nodes

        Dim tempLastIndex
        tempLastIndex = lastItemIndex     'Make a temp for the last index number
       
        ' Check if there is a previous node
        If not isnull(collection(lastItemIndex).prevNode)  Then   
                ' Set the reference for the last item to the index of the left neighbour
            lastItemIndex = collection(collection(lastItemIndex).prevNode).index
                ' Set the reference for the next node of the left neighbour to null
            collection(lastItemIndex).nextNode = null
        End If

        ' destroy the node element
        Set collection(tempLastIndex) = nothing

        ' decrement the node counter
        nodeCount = nodeCount - 1
    End Sub

    Public sub deletefirst()

        If nodeCount = 0 Then exit sub
       
        Dim tempFirstIndex
        tempFirstIndex = firstItemIndex

        ' Check if there is a next node
        If not isnull(collection(firstItemIndex).nextNode)  Then
                ' Set the reference for the first item to the index of the right neighbour
            firstItemIndex = collection(collection(firstItemIndex).nextNode).index
                ' Set the reference for the previous node of the right neighbour to null
            collection(firstItemIndex).prevNode = null
        End If

        Set collection(tempFirstIndex) = nothing
        nodeCount = nodeCount - 1
    End Sub

    Public property get count()
        'return the amount of nodes
        count = nodeCount
    End Property
   
end class 


Some test statements to show how it works::


Dim ll, mc
Set ll = linkedList

    ll.add "first"
    ll.add "second"
    ll.add "third"
    ll.add "fourth"
    ll.add "fifth"
   
msgbox ll.getFirst()            ' >first
msgbox ll.getLast()             ' >fifth

' Delete the first item
ll.deletefirst
msgbox ll.getFirst()            ' >second

msgbox ll.count                 ' >4

' Delete the last two items
ll.deletelast
ll.deletelast
msgbox ll.getLast()             ' >third


The linked list class makes it very easy to create stacks and queues. Here is an example of how to create a stack:


Public function stack()
    set stack = new clsStack
End Function

Class clsStack

    Private ll
    Private Sub Class_Initialize    ' Setup Initialize event.
        set ll = linkedList         ' Create a linked list instance
    End Sub

    Public function pop()
        If ll.count = 0 Then exit function    ' No items in the list
        pop = ll.getlast()          ' Return the last item
        ll.deleteLast               ' and delete it
    End Function

    Public sub push(element)       
        ll.add element              ' Add an item to the list
    End sub

    Public function peek()           
        peek = ll.getlast()         ' Peek at the top item
    End Function

End Class

And some sample code for a little demonstration:

Dim myStack
set myStack = stack         ' Set as new stack

myStack.push "item1"        ' Add some items
myStack.push "item2"
myStack.push "item3"

msgbox myStack.pop()        ' >item3
msgbox myStack.peek()       ' >item2
msgbox myStack.pop()        ' >item2 

The thing that is missing for generic use is an insertbefore() and an insertafter() method on the linked list. But to implement this, you'll need a virtual reference table that maps the collection() array index to a chronologic ánd continuous index. Another way to do this is a looping mechanism where you can walk through the elements with a getnext() or getprevious() method.
For now, the stack and queue functionality is sufficient for my needs.