Akien MacIain, Senior Automation Engineer, Delta Dental of
California
Table Of Contents
Adding Properties to a BagClass
Creating BagInstances of a BagClass
Calling methods and properties
Introduction
For the entire time I’ve been working with HP QuickTest Pro and VBScript, I have had a yearning for real object support. Specifically, inheritance and polymorphism. I am a big fan of reusing every piece of code I can, and these two attributes contribute more to my productivity than any other concept in software development.
In addition, at one point when we were performing a refactoring of our automation framework, I found myself implementing a port of the Log4j logging framework in VBScript (for more on log4j see http://logging.apache.org/log4j/1.2/index.html and http://en.wikipedia.org/wiki/Log4j). To do that effectively I needed the ability to pass in varying numbers of parameters to methods and I needed inheritance.
Overview
To implement this kind of functionality, I went back to what I knew of the internals of a typical C++ implementation. Internally, the compiler sees objects as a collection of metadata, pointers to functions, function prototype maps, and so on. To recreate this, I created a VBScript class called BagDict. Internally a BagDict is made up of two standard Scripting.Dictionary objects, one for object data, and one for metadata. The BagDict implements the basic Scripting.Dictionary interface, which access the object data. We then go on to implement a group of additions to the basic interface, which allow us to create VBScript++ classes, instances, add methods and properties to classes, and call those methods and properties. The BagDict is the substrate that BagObjects are built on.
One piece of terminology we need before going on is the distinction between an object class and instance, in the VBScript sense, and a VBScript++ BagObject, BagClass and BagInstance. BagObjects are instances of the BagDict VBScript class which contain additional metadata that makes them either a BagClass or a BagInstance. BagClasses and BagInstances are the VBScript++ counterparts to the traditional VBScript class and instance. BagObject refers to an item which could be either an instance or a class.
Before we start looking at how all this works in practice, there is one more item we need to cover. As I mentioned above, one of the requirements when I started this project was I wanted to be able to pass a variable numbers of parameters into things, so I could have evolving functionality but the static interfaces that VBScript requires. I took my inspiration from Perl, which allows you to pass an anonymous hash in to your functions.
As an example, let’s look at the following code:
Function DoSomething (args)
BagDictCreate args, NULL
Print args(“my_data”)
End Function
DoSomething "my_data=>thing to print|some_other_data=>foo"
The BagDictCreate function takes the args variable by reference (which is a string) and converts it into a BagDict. Each key/value pair in the string is separated by a pipe (|) and the => symbol is used to separate the keys from the values. You can also pass in arrays where the first entry is a key, the second a value, the third a key, and so on. BagDict instances created this way are not BagObjects, even though they are instances of the VBScript BagDict class. They lack the metadata to be BagObjects.
As a matter of style, we refer to keys passed in this way in the documentation either as key:my_data, object key:my_data or metadata key:my_data. Whenever object or metadata is not specified, you should assume the key refers to object data. This is reflected in the documentation included in the code.
The BagObject Framework
Creating new BagClasses
In the VBSCript++ model, everything inherits from ClassBase. To create a new class, we do this:
Dim classFileSystemItem
Set classFileSystemItem = ClassBase.MakeClass ("ClassFileSystemItem", NULL, _
"self.is_virtual=><eval>True")
There are three parameters to the MakeClass method:
· The first parameter identifies the name of the new class we are creating, in this case ClassFileSystemItem. Does not have to match the name of the variable it’s stored in. (Though in practice I always use the same names for the two to avoid confusion.)
· The second parameter is any object data we wish to add at this point, in the same form that we would pass in to BagDictCreate. In this case, there isn’t any, so we pass in NULL.
· The third parameter is any metadata we might wish to add at this point. In this case, we are adding self.is_virtual, which we set to True (virtual classes are not allowed to be instantiated, only inherited from). The <eval> keyword means that this expression will be evaluated in the global namespace before the assignment to the new BagDict.
The result of this call is a new BagObject of type BagClass that we store in to the classFileSystemItem variable.
Adding Methods to a BagClass
Now that we have our new class, we can begin adding methods and properties to the class. To add a method to the class, we first need to create a new, non-virtual instance of our base class, and then call the ApplyMethod routine of our new class.
Dim classFile
Set classFile = classFileSystemItem.MakeClass("ClassFile", NULL, _
"self.is_virtual=><eval>False" )
We have now created a new concrete class, called ClassFile, that inherits from ClassFileSystemItem. By setting self.is_virtual to False we’ve ensured that we can actually create BagInstances of this class. Now let’s add the method. We add methods to the class by calling ApplyMethod:
classFile.ApplyMethod "Delete", NULL
Function ClassFile_Delete(self,args)
self.VerifyHasKeys "file_name", "ClassFile_Delete fails missing key:file_name"
If ClassFile_Exist_Get(self, args) Then
ClassFileSystemItem_FSO.DeleteFile(self("file_name"))
End If
End Function
Please note that all methods and properties in VBScript++ are implemented as functions. They’re all called by Eval() which requires them to be functions and not subs. Please note that they don’t have to be functions that return values.
The first parameter to AddMethod provides the name this class will use to refer to the method, in this case Delete.
The second parameter is an anonymous Hash. We could use the key:vector to tell VBScript++ what function to call whenever the Delete method of class ClassFile is called. We call this the method vector. This is a VBScript version of function pointers. If the second parameter is NULL, as it is here, then ApplyMethod will generate the method vector by combining the name of the class, an underscore and then the method name. In our example, the method vector is ClassFile_Delete. If we wanted the Delete method to instead vector to a function called ClassFile_DeleteFile, our ApplyMethod statement would have looked like this:
classFile.ApplyMethod "Delete", "vector=>ClassFile_DeleteFile"
It is important to notice that the ClassFile_Delete method takes two arguments, self and args. The first argument, self, is the BagInstance that has just vectored to this function. The args parameter is any other arguments that the programmer has elected to pass in. However, notice that in our example above, BagDictCreate is NOT called. It is actually called inside the object framework before vectoring to the method code, so args in this case is already a BagDict.
In addition, in the above example, you will notice self.VerifyHasKeys. This method verifies that the specified BagDict has the needed keys before continuing, and if it does not, issues an error message. This is important to do because in VBScript, and in particular in this object implementation, the runtime does not do most of the compiler time checking and verification that some languages provide for you.
Adding Properties to a BagClass
Adding properties to a class is done mostly the same way as methods. In the ClassFile_Delete method above, there is a call to a Exist property (If ClassFile_Exist(self, args) Then). We can define this property as follows:
classFile.ApplyProp "Exist","get","return_type=>Boolean"
Function ClassFile_Exist_Get(self,args)
self.VerifyHasKeys "file_name", "ClassFile_Exist fails missing key:file_name"
ClassFile_Exist = ClassFileSystemItem_FSO.FileExists(self("file_name"))
End Function
It’s important to note that using the direct method shown in the example above (If ClassFile_Exist(self, args) Then) means that the inheritance mechanism is bypassed, if we overrode the mechanism for exist, the override would not be used. Sometimes, this is the behavior we want, but sometimes we also want to take the most recent definition, which we can do like so:
If self.Prop("exist", null) Then
…
When we create a property, please note that there are 3 arguments instead of two. In the example above, the second argument is "get", this could be get, set or let. Though internally set and let are the same, and whether the return data is an object is handled automatically. So you only need 2 not 3 property handlers.
For Setters, a third value is passed in. This is the only exception to the two argument rule. The third parameter is the value to be used by the setter.
So for covering both bases, we’d do something like this:
myClass.ApplyProp "time","get", "return_type=>Boolean"
Function myClass_Time_Get(self,args)
…
myClass.ApplyProp "time","set", NULL
Function myClass_Time_Set(self,args,value)
…
Creating BagInstances of a BagClass
Now that the ClassFile BagClass has been created, it’s time to look at creating instances (BagInstances) of our new class. We create instances of a class by calling the classes static MakeObject method.
Dim myFile
Set myFile = classFile.MakeObject ("file_name=>c:\foo.txt", NULL)
That is all there is to it. We ask the class to make a new instance and we pass in any object data (in this case, file_name) and metadata (in this case NULL). These are applied to the new BagInstance object before it is returned.
Calling methods and properties
Now that we have an instance of the ClassFile class, we can call it’s methods using instance.Method(). The first parameter to Method() is the name of the method we want to call, and the second parameter is any arguments we want to pass in, in a format the BagDictCreate understands.
myFile.Method "Delete", NULL
Methods can return values. If we modified Delete() to return a boolean about the success of the operation, we could do this:
myData = myFile.Method("Delete", NULL)
One important note here is that we do have to maintain the VBScript rules about parenthesis or not based on whether or not we’re doing anything with the return value (VBScript doesn’t actually care of it’s a function, if you’re not doing anything with the result, you must call it as if it were a Sub).
If we wanted to pass in an argument, we might do it like so:
myFile.Method "Delete", "force=><Eval>True"
If we wanted to get or set a property, we might do it like so:
myVar = self.Prop("date_accessed", null)
or:
self.Prop("date_accessed", null) = myVar
How it’s most commonly used
I just wanted to post a little example here. In this case, the objects represent the records that the system under test is designed to manipulate. The UI manipulation happens inside the method call, so the test cases deal only with the data abstractions. This means that as the application under test changes, the libraries change, but the test cases do not.
'Support library
Dim ClassMember
ClassMember = ClassBase.MakeClass("ClassMember", NULL, NULL )
ClassMember.ApplyMethod "add",NULL
Function ClassMember_Add(self, args)
'(function code here)
End Function
'test case code
Dim newMember
newMember = ClassMember.MakeObject (NULL, NULL)
newMember("last_name") = "Smith"
newMember("first_name") = "Bob"
newMember.Method "add",NULL
Custom Signatures
Another feature common to most object based languages is polymorphism. This allows the compiler to look at different method calls with the same label and route them based on the types of the data passed in. So you might have an Add() method, that has two different signatures, and the compiler does the right thing with a call based on it’s arguments. Add(myMember) might do something very different than Add(myMember, phoneNumber).
Because all of the method and property calls in VBScript++ are required to take only two arguments (self and args), to support the middleware properly, we can’t change the behavior based on the number of arguments or the types of the arguments. It’s also the case that VBScript++ is a loosely typed language, so typing might not be meaningful in any case.
However, we do have named keys as a way to distinguish between the arguments passed. We can create calls for different methods based on the passed keys. Like so:
myClass.ApplyMethod "Create", _
"required_keys=>company_name|vector=>MyClass_Create_Company"
myClass.ApplyMethod "Create", _
"required_keys=>last_name|vector=>MyClass_Create_Person"
And then, when we make our calls, we can do like so:
MyObject.Method "Create","company_name=>foo" ' goes to MyClass_Create_Company
MyObject.Method "Create","last_name=>Smith" ' goes to MyClass_Create_Person
Even clearer, we can do this:
Dim myRecord ' make container
BagDictCreate myRecord, NULL ' turn it into record
myRecord("last_name") = "Smith" ' add field
myRecord("first_name") = "Bob" ' add another field
MyObject.Method "Create",myRecord ' call method
Constructors and Destructors
The framework supports constructors and destructors. Both take the same two arguments as the rest of the bag methods.
Default constructors are called <classname>_Constructor. If your instance requires instantiation of an instance specific object, it should be created in the constructor, as any created at the class level will be shared amongst all the entities in the class.
Likewise, destructors are called <classname>_Destroy.
Debugging
There are two important points about debugging. The first is where you set your breakpoints. Because of the exec() call inside method and property calls, you must set your breakpoint in the target routine, you can’t assume that you’ll be able to step into it.
In the code, there’s a special comment you can search for: JumpStation
This string happens just above where the vector call is made. If you set a breakpoint on the vector call statement just below this comment, you can check the value of the variable called stringCommand, and then go and set a breakpoint in that target routine.
The second point was something I stumbled across as I was developing this. I found I needed to be able to "see" into the objects when debugging. I could set watchers for each key, but sometimes I just wanted to see the whole thing at once. Toward that end, I created Debug().
myRecord.Debug()
This returns an array of all the keys and string approximations of their values. Metadata keys are prefixed with M: and object data keys are prefaced with O:
Other little amusing oddities
One of the things I really like about this framework is that I can add a method to an instance, without creating an intermediate class. I can also create a class, create an instance of the class, pass that instance to a caller, and allow the creating class to go out of scope without impacting the instance that was passed back.
For all it’s delights however, it also is a framework that must be used with care, because for the most part, it does not protect you from yourself.
How to shoot yourself in the foot with VBScript:
Write a shoot.vbs virus and attach it to an e-mail. Two hundred million Microsoft Outlook users will infect their feet with it.
How to shoot yourself in the foot with VBScript++
With what?!?!
Summary
I’ve seen some post processing ways to do this in VBScript, but I found them cumbersome to use and difficult to debug. This framework gives me the best of all worlds. I can debug with it, I can create classes and instances that reflect the abstraction structures that are relevant to the applications I’m testing, and most importantly, I can reuse almost all of what I do. Without shooting myself in the foot too much.
You’re welcome to use this framework, subject to the GNU General Public License version 3 (GPLv3) (http://www.opensource.org/licenses/gpl-3.0.html). I am up for answering questions as time permits, but offer no promises about it’s fitness for any particular purpose, nor do I commit to issuing bug fixes (though if you tell me of one, I probably will fix it).
Attachments
The download for this article contains one file (aggregations.logging.vbs), which is an aggregation of several files:
· ExtensionsBase.vbs: A general library of extensions to VBScript. Handy in lots of ways and is mostly needed for the VBScript++ implementation.
· ExtensionsStrings.vbs: A general library of string specific extensions to VBScript. Also handy.
· ExtensionsFiles.vbs: File related VBScript extensions.
· ExtensionsDates.vbs: Date string manipulation extensions.
· VBScript++.vbs: The actual VBScript++ implementation, including the BagDict class, a number of required support functions, and the basic objects we’ve defined so far.
· Logging.vbs: This file contains Log4VB, which is our limited implementation of Log4J. It does not include the configuration from XML capability, but does provide a useful example of using VBScript++.
You can download aggregations.logging.vbs here.
Feel free to contact Akien with any questions or comments:
Posted in Using Classes, VBScript Techniques


Yaron Assa




January 11th, 2010 at 9:02 am
Very nice thinking, and nice libs too. Although I encountered some issues while scrolling through them:
The AssignIfEmpty routine errors when variableToSet is an object that Is Nothing. It evaluates (variableToSet = “”) and will return an ‘Object variable not set’ error.
Cause: VBScript does not lazy handle the OR operator, but will handle each condition after the other, even if the former is already True. To circumvent this, you’ll have to branch the if statement.
Also the MakeDict routine needs some checking; It checks if an argument is a “<cast:” but uses variables (temp, temp1, temp2) that are never declared. Also the temp variable is never defined, so basically, the check for casting will error or otherwise (On Error Resume Next) not work correctly.
I know, VBScript is a bitch.
January 12th, 2010 at 1:02 am
Odd thing is, I don’t get errors when I execute those. Tho I may not have passed Nothing in to AssignIfEmpty… I’ve fixed that, thanks for pointing it out.
I just went thru the code, and there is no MakeDict, there is a DictMake, and the 7th line of the function has the Dim for those vars. You can search for “Dim passedStringArgs, passedArrayArgs, temp1, temp2″
Thanx!
January 27th, 2010 at 1:21 am
Hi Akenm … kinda of odd question to put … but can u let me know .. this framework will be helpful in which situations … i was not able to relate it :(
February 23rd, 2010 at 2:38 am
If you use QTP only in record/playback mode, this won’t help you.
On the other hand, if you’re doing layered abstractions to distance the test code from the interfaces being tested, this can be helpful in the design of your layers.
One piece I used it for was the logging (the code is included in the download, but will be the subject of it’s own article shortly). Another was for event based testing, and the article for that has been submitted to advancedqtp.com, but they haven’t had time to post it.
If you’ve never worked with object oriented programming, then this is probably not the thing for you. If you have a more specific question, please feel free to ask here.
March 17th, 2010 at 10:09 pm
Thanks for your reply akienm … i am not using record and playback but using a keyword driven framework for automation … so can you help me understand where will this vbscript++ help me ??
April 13th, 2010 at 9:58 pm
Only if you’re building your own keyword system. You could, for instance, implement each keyword as an object. If you’re using the built in one, it assumes actions are the “building blocks”.
I posted something else here which might better express what this object framework could be used for: http://www.advancedqtp.com/knowledge-base/articles/code-techniques-id15/code-design-id16/using-classes-id30/event-manager/
In most automation, the order the software does things is important. But to us humans, much less so. The event system makes the testware tolerant to changes in the order things happen. Give it a read and see if VBScript++ makes more sense or not.
April 24th, 2010 at 12:43 pm
Good work..
April 24th, 2010 at 6:48 pm
Very Nice
I just downloaded aggregations.logging.vbs
2 questions
1.Do you have a small demo program that uses your BagClasses that can be run without QTP? and if so could you send it to me, I really learn better by running a program the works.
2. Do you know of a good (free vbs++) editor?
Thank you for your great work.
April 27th, 2010 at 9:20 pm
It was generally written in such a way that it should not *require* QTP… Much. I’ve written very little stand along VBS… I can think of one or two things that you might need to tweak in the *rest* of the library, tho not in the VBScript++ part itself. Specifically, there’s a timer thingie in the libraries that I believe uses one of QTPs functions, and there’s a GlobalDictionary that *might* be referenced somewhere. It’s just a regular dictionary object, instantiated globally and called GlobalDictionary.
As to editors, no, I know of no VBScript++ specific editor. Around here we’re using either the built in editor in QTP *or* Notepad++. Which I am very fond of.
June 7th, 2010 at 10:53 am
So far, I can’t fully understand the concept of VBScript++.
I hope that it would be better after looking through the txt example given as attachment.
July 10th, 2010 at 7:44 pm
Interestingly, I can never get this to work at all with QTP 10.0. The logic always fails when the line is run:
classBase.ApplyMethod “Destroy”, Null
When I trace the error, it’s happening the VectorCalculationColumn routine. Specifically, the line
If args.Exists(”args”) Then …
Errors with: Object doesn’t support this property or method: ‘args.Exists’.
It looks like that when that logic is run (in the Vector method), it calls MakeBag and — with that “ApplyMethod ‘Destroy’ line ONLY — is not getting anything back in the args variable.
So, unfortunately, I can’t do a thing with this to evaluate it. Yet no one else is mentioning this, so I have to assume I’m the only one. Is there updated code somewhere that maybe I’m not seeing?
July 10th, 2010 at 7:46 pm
(Make that the VectorCalculationCommon routine in my above post. I incorrectly said “Column” for “Common.”)
July 11th, 2010 at 12:50 pm
Never mind! It turns out there is more than one version of this logic floating around; the one I had didn’t include some of the key override methods for the standard dictionary object. Without those, the object data and metadata weren’t getting populated correctly, hence the problem I reported.
July 12th, 2010 at 7:41 pm
So has anyone ever actually gotten this to work ALL the way through? Even with the provided examples, I only find that it works partially. For example, the filesystem examples (where a foo.txt file is deleted or checked in terms of its existence) never seem to work.
The logic all passes without failure; but the file is not deleted and the existence is not truly checked.
I’m worried because I’ve found definite bugs in the code. For example, one of the simplest is the CopyFrom routine. It looks like this:
Sub CopyFrom (byref sourceDict, args)
CopyKeys targetDict, localData, args
End Sub
Notice how targetDict is passed to CopyKeys, but the argument to CopyFrom is sourceDict? My guess is this was just a paste problem between CopyTo and CopyFrom.
But it also means this would not have worked.
There are a few other things here and there like this that I’ve found so I’m worried there’s inherent bugs in the logic itself beyond anything I’m trying to do with it.
July 14th, 2010 at 5:10 pm
I’m probably mostly talking to myself but in case anyone else comes along to use this, I found another bug in the provided logic. This one is in DictMake and is this line:
temp1 = Mid(temp1, 1, InStr(1, temp, “>”) - 1)
If you leave it as temp, you can get an Invalid procedure call or argument to Mid error for the obvious reason that temp is not defined with anything.
The reason for this is line directly above it, which looks like this:
temp1 = Mid(newHash(i+1), 7)
That should be:
temp = Mid(newHash(i+7), 7)
That way if I have something like “system_response=>1″, then
temp1 = “integer”
temp2 = “1″
July 14th, 2010 at 5:13 pm
That last comment got parsed due to html tag. Before the >1 in that message, I had a <cast:integer.
August 4th, 2010 at 1:56 am
Jeff, please feel free to write me at akienm@gmail.com
I’ll happily fix any bugs :)
Thanx!
August 4th, 2010 at 1:59 am
As I keep reading back thru Jeff’s responses, yes, I’m using this every day. I have another article on this that I’m working on where I describe the logger we’ve built, part of which is based on Log4J. It’s also the case that the event manager posted elsewhere on this site uses this code.
Please write me at akienm@gmail.com and I’ll be happy to help and/or provide fixes.
August 4th, 2010 at 2:00 am
As to the sourceDict targetDict thing, the copy only takes one argument, because it’s copying to or from itslef.
I do need to make clear we haven’t moved to QTP10 so I haven’t tested it there.
August 26th, 2010 at 9:30 am
Hi Akenm, nice thinking! VBScript must be expanded if we do not expect it to die.
When I tried to call vbscript++ from QTP, I encountered an questions.
Q1: the classes static MakeObject method did not exist
I can not search “MakeObject” in the library provided by you, but another method “NewObject” was found. Is it a right method name for “MakeObject”?
Q2: When running the following code, an error message were printed: “TBD: ERROR (13 Type mismatch in Microsoft VBScript runtime error) ENCOUNTERED CALLING ClassMember_add (me, args)”
‘**************************************************
‘VBS code:
‘**************************************************
Dim classMember
Set classMember = ClassBase.MakeClass(”ClassMember”, NULL, “self.is_virtual=>False” )
classMember.ApplyMethod “Add”,NULL
Function ClassMember_Add (self, args)
msgbox “Function: ClassMember_Add was called successfully!!!”
End Function
‘test case code
Dim newMember
Set newMember = classMember.NewObject(NULL, NULL)
newMember.Method “Add”,NULL
‘**************************************************
A thing was confused me:
The statement “eval(”ClassMember_Add (me, args)”)” was called successfully in QTP action, but it failed running in the vbscript++ library (In the Function “VectorCall”).
Can you please help me answer two questions?
Thanks in advance.