4
votes

I'm trying to put together tests in HP Unified Functional Testing the way a programmer would. For those unaware, the tool uses VBScript as its driver.

Because I want to use data from the same DataTable across multiple UFT actions -- and because the Global table already has a different set of data on it -- I want to retrieve data from an external file. UFT happily supports this function.

My current plan is that, depending on which test I'm running, I will iterate through only a range of rows in that table.

This is the script I've come up with:

' targets the local sheet, but 
' not the same value as dtLocalSheet
Const sheetNum = 2 

dim sheetRowCount
DataTable.ImportSheet "PersonFile.xlsx", 1, sheetNum
sheetRowCount = DataTable.GetSheet(sheetNum).GetRowCount

dim firstRow, lastRow
firstRow = Parameter("FirstPersonIndex")
lastRow = Parameter("LastPersonIndex")
If sheetRowCount < lastRow Then
    lastRow = sheetRowCount
End If

If sheetRowCount >= firstRow Then
    Dim i
    For i = firstRow To lastRow
        DataTable.SetCurrentRow i

        ' begin payload
        MsgBox(DataTable.Value("LastName", dtLocalSheet))
        ' end payload

    Next
End if

I don't want to have to repeat all this boilerplate every time I want to use this pattern. I'd really like to have something like:

In a Function Library:

sub LoopThroughSheetAnd(sheetFile, doThis)
    ' targets the local sheet, but 
    ' not the same value as dtLocalSheet
    Const sheetNum = 2 

    dim sheetRowCount
    DataTable.ImportSheet sheetFile, 1, sheetNum
    sheetRowCount = DataTable.GetSheet(sheetNum).GetRowCount

    dim firstRow, lastRow
    firstRow = Parameter("FirstRow")
    lastRow = Parameter("LastRow")
    If sheetRowCount < lastRow Then
        lastRow = sheetRowCount
    End If

    If sheetRowCount >= firstRow Then
        Dim i
        For i = firstRow To lastRow
            DataTable.SetCurrentRow i

            call doThis()
        Next
    End if
end sub

In the original action...

sub Payload1()
    MsgBox(DataTable.Value("LastName", dtLocalSheet))
end sub
LoopThroughSheetAnd "PersonFile.xlsx", Payload1

In a separate action, 3 or 4 steps later...

sub Payload2()
    ' compare the data against another data source
end sub
LoopThroughSheetAnd "PersonFile.xlsx", Payload2

The above code doesn't work in VBScript. A type mismatch error is thrown as soon as we try to pass Payload1 as a parameter.

How could one reasonably pull this off in VBScript? Bonus points if the answer also works in UFT.

4
Sane question/problem. - Ekkehard.Horner

4 Answers

7
votes

You can pass functions as parameters with the GetRef() function. Here's a utility map function, like you'd find in JavaScript that accepts an array and calls a function for each element of the array:

Sub Map(a, f)
    Dim i
    For i = 0 To UBound(a)
        ' Call a function on each element and replace its value with the function return value
        a(i) = f(a(i))      
    Next
End Sub

Map MyArray, GetRef("SomeFunc")

Now you could write SomeFunc so that it operates on a value and returns an updated value:

Function SomeFunc(i)
    SomeFunc = i + 1
End Function

This works fine. map calls SomeFunc using the function "pointer" we passed to it.

You could do something similar with your LoopThroughStreetAnd function:

LoopThroughStreetAnd "PersonFile.xlsx", GetRef("Payload2")
2
votes

The standard way of callbacks in VBScript uses GetRef, as in this demo.

0
votes

When using objects, you can wrap a call to a method in an object, and then you can pass the object. (This is approximately what happens in other languages already, you just have to do it manually in VBScript.)

The only issue is that any method called this way has to be Public.

I would use a naming scheme of something like "Func1", "Func2", "Action1", "Action2", etc., depending on the arity of the functions and whether they return values or not.

Dim s : Set s = New Something : s.Run

Class Something
   Public Sub HowToPassMe(pValue)
      WScript.Echo pValue
   End Sub

   Public Sub Run
      Dim action : Set action = New Action1Wrapper
      Set action.Target = Me
      Dim se : Set se = New SomethingElse
      se.DoSomethingElse action
   End Sub
End Class

Class SomethingElse
   Public Sub DoSomethingElse(pAction1)
      pAction1.Action1("something")
   End Sub
End Class

Class Action1Wrapper
   Private mTarget
   Public Property Set Target(value) : Set mTarget = value : End Property

   Public Sub Action1(p1)
      mTarget.HowToPassMe(p1)
   End Sub
End Class

Using Execute, Action1Wrapper can also be written something like the following. You can also write a factory class for easier use.

Class Action1Wrapper
   Private mTarget
   Public Property Set Target(value) : Set mTarget = value : End Property

   Private mName
   Public Property Let Name(value) : mName = value : End Property

   Public Sub Action1(p1)
      Execute "mTarget." & mName & "(p1)"
   End Sub
End Class

Class Action1Factory_
   Public Function Create(pTarget, pName)
      Dim a1 : Set a1 = New Action1Wrapper
      Set a1.Target = pTarget
      a1.Name = pName
      Set Create = a1
   End Function
End Class
Dim Action1Factory : Set Action1Factory = New Action1Factory_

Used as:

Dim action : Set action = Action1Factory.Create(Me, "HowToPassMe")
Dim se : Set se = New SomethingElse
se.DoSomethingElse action
-1
votes

And as I write the question, my memory gets jogged, and I begin researching a "feature" I once discovered.

This fails to work in the context of HP UFT, but if you're running cscript, or working with Classic ASP, you can either declare a function late, or replace a previous declaration, to change how it works.

VBScript lets you declare the same function or subroutine multiple times in a program. It treats the last declaration as the correct one.

You can get around this in cscript and ASP by physically separating the different versions of the function, so that one doesn't get clobbered by the other. You'll have to be careful not to put the two anywhere near each other, or you(r successor) might have an aneurysm trying to debug the outcome.

Honestly, you're probably better served refactoring your code some other way.

Now, with the disclaimers out of the way, the following example is for use with cscript or wscript.

Code

Since this won't work in UFT anyway, I'll write from scratch.

In WrapperSub.vbs:

' Sub WrapperSub_Payload doesn't exist in this file.
' It must be declared by the calling file or the program will crash.
Sub WrapperSub()
    wscript.echo("This begins the wrapper.")
    WrapperSub_Payload
    wscript.echo("This ends the wrapper.")
End Sub

In WrapperSubUseA.vbs:

With CreateObject("Scripting.FileSystemObject")
    call ExecuteGlobal(.openTextFile("WrapperSub.vbs").readAll())
End With

Sub WrapperSub_Payload
    wscript.echo("This is payload A.")
End Sub

WrapperSub

In WrapperSubUseB.vbs:

With CreateObject("Scripting.FileSystemObject")
    call ExecuteGlobal(.openTextFile("WrapperSub.vbs").readAll())
End With

Sub WrapperSub_Payload
    wscript.echo("This is payload B.")
End Sub

WrapperSub

Output

>cscript wrappersubusea.vbs
Microsoft (R) Windows Script Host Version 5.8
Copyright (C) Microsoft Corporation. All rights reserved.

This begins the wrapper.
This is payload A.
This ends the wrapper.

>cscript wrappersubuseb.vbs
Microsoft (R) Windows Script Host Version 5.8
Copyright (C) Microsoft Corporation. All rights reserved.

This begins the wrapper.
This is payload B.
This ends the wrapper.

Note that if a placeholder for WrapperSub_Payload were declared in the source file, that placeholder would always execute instead of the intended subroutine. This is probably due to ExecuteGlobal executing after the current file is parsed, causing the placeholder to load after the local declaration.

When you try this in UFT -- placing the contents of WrapperSub.vbs in a function library -- the function library rightfully ignores the caller's scope. It will then fail because WrapperSub_Payload doesn't exist in scope.