Featured

QTP 10 not Recognizing Oracle Forms Obje

Bidun Meduza shared on our Facebook page a problem and solution that I thought might be useful to you too. Problem He had a situation where QTP 10 was unable to recognize objects of Oracle Forms. Th

Read More
QTP 10 not Recognizing Oracle Forms Objects

New Navigation Menus

In order to ease orientation through our vast knowledge archive, we have added the following navigation menus: First Steps Scripting QTP VBScript Tutorial Knowledge Articles (by levels of expertise) B

Read More
New Navigation Menus

Permalinks

We’ve changed the permalinks format, so if you have been experiencing some trouble with finding materials with the old links, then remove the YYYY/MM/ from the URL and you’d be fine. In ca

Read More
Permalinks

Advanced QTP on Android

A first version of AdvancedQTP for Android can be found here. Now you can take this perfect companion with you to keep updated on what’s new in our site.

Read More
Advanced QTP on Android

New Advanced QTP Features: Layout and FB

As you have most surely noticed, we’ve recently changed the site’s theme in order to enhance the overall user experience. As from today, you can also register and login seamlessly with you

Read More
New Advanced QTP Features: Layout and FB Integration

Class Delegation

0
by on May 29, 2010 at 11:44

Steve Windom has graciously shared the following article on Class Delegation.

We’ve previously touched upon this subject under Class Composition and Function Pointers articles, but Steve’s article provides a coherent and clear view.

Intent: In some situations, using inheritance to extend a class leads to a bad design. Though less convenient, delegation is a more general-purpose way to extend classes. Delegation works well in many situations where inheritance does not work well. For the purposes of test automation, delegation can be a critical technique because some test automation tools, like QTP do not support inheritance.

Context: Inheritance is a common way of extending and reusing the functionality of a class. Delegation is a more general way to extend a classes behavior. Delegation extends a class by using another classes functions, subroutines, and properties rather than inheriting them. Inheritance is inappropriate for many situations in which delegation works well.

For example, inheritance is useful for capturing a “is-a-kind-of” relationships because of their very static nature. However, “is-a-role-played-by” relationships are akward to model with inheritance. With delegation you get instances of classes that can play multiple roles.

Applicability:

  • Pro - Inheritance is a static relationship; it does not change over time. If you find that an object needs to be a different subclass of a class at different times, then it should not be a subclass of that class in the first place. If an object is created as an instance of a class, it will always be an instance of that class. However, an object can delegate behavior to different objects at different times.
  • Pro - Much or even most reuse and extension of a class is not appropriately done through inheritance.
  • Pro - The behavior a class inherits form is base class cannot easily be changed over time. Inheritance is not useful when the behavior a class should build on is not determined until run time.
  • Pro – Inheritance is not supported by some test automation tools such as QTP, so delegation is the only viable option to inheritance.
  • Con - Delegation imposes less structure on classes than inheritance. In designs where it is important to constrain the structure of classes, the structure and inflexibility of inheritance may be a virtue. This is often the case in frameworks.
  • Con - Delegation can be less convenient than inheritance because it requires more code to implement.

Implementation: VBScript as a language does not provide any mechanism for inheritance or polymorphism. However, delegation allows us to fake it. We do this by creating an instance of a theoretical parent class or (the Delegator) within the original class (the Delegatee) and calling its methods. We then call the parent classes functionality in the “inherited” child classes methods which simply call (or delegate to) the same method of the parent class.

 

image

Delegation is a more general purpose approach than inheritance. Any extension to a class that can be accomplished by inheritance can also be accomplished by delegation.

Here is a code example: (Note: The examples and code presented in this document are QTP/VBScript examples.)

<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> cDelegator()
      <span style="color: #0000ff;">Set</span> cDelegator = New Delegator
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

<span style="color: #0000ff;">Class</span> Delegator
      <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Sub</span> SayHello()
            <span style="color: #0000ff;">MsgBox</span>("<span style="color: #8b0000;">Hello There!</span>")
      <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> Delegatee
      <span style="color: #0000ff;">Dim</span> cDelegator

      <span style="color: #0000ff;">Private</span> <span style="color: #0000ff;">Sub</span> Class_Initialize
            <span style="color: #0000ff;">Set</span> mDelegator = cDelegator()
      <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

      <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Sub</span> SayHello()
            mDelegator.SayHello()
      <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

As you can see, the SayHello() method in the Delegatee class really uses an instance of the Delegator Class to implement it’s SayHello() functionality.

Consequences: Delegation can be used without the problems that accompany inheritance. Another advancage of delegation is that it is easy to compose behavior at run time.

The main disadvantage of delegation is that it is less structured than inheritance. Relationships between classes built using delegation are less obvious than those built using inheritance. Here are some strategies for improving the clarity of delegation based relationships.

  • Use consistent naming schemes to refer to objects in a particular role. For example, if multiple classes delegate the creation of widget objects, the role of the delegatee object becomes more obvious of al the classes that delegate that operation refer to delegatee objects through a variable called widgetFactory.
  • Clarify the purpose of a delegation by writing comments.
  • In extreme cases, these indirect delegations can make an intermediate class less coherent by adding functions, subroutines, or properties unrelated to the class’s purpose. In such cases, refactor the unrelated functions, subroutines, or properties into a separate class.
  • Use well-known design and coding patterns. A person reading code that uses delegation will be more likely to understand the role that objects play if the roles are part of a well-known pattern or a pattern that recurs frequently in your program.

Note: It is possible and advantageous to use all three of these strategies at the same time when you can.

References:

  1. Mark Grand, Brad Merrill (2005). Visual Basic Design Patterns: Wiley Publishing, Inc.
  2. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (1995). Design Patterns Elements of Object-Oriented Software: Addison-Wesley.

, , ,

Event Manager

0
by on March 12, 2010 at 21:00

By Akien MacIain
Senior Automation Engineer, Delta Dental of California

Table Of Contents

Table Of Contents

Introduction. 2

Assumptions. 2

Overview. 3

Implementation. 4

Let’s Start With Events. 4

A Quick Word On Queues. 5

Now For An Example. 5

A Note On This Design. 6

Advanced Topics. 6

More Notes About Events And Queues. 6

On Speed. 6

On Designing HasContextStart7

On Referencing Each Other7

On Completion. 7

Errors And Error Handling. 8

Detailed Queue Processing. 9

Tricks And Techniques. 9

A (more) Complete Reference. 9

Summary. 9

Attachments. 9

 

Introduction

Most of the time in test automation, we try to work with very predictable workflows, where each event happens in a predictable order. The worst case we usually have to deal with is expected error conditions, at which point in our workflow, we add code to detect the error, and direct our code to an error handling path.

 

However, it’s also the case that getting these workflows correct can often take a lot of trial and error. In addition, there are usually unexpected error conditions, that when encountered, necessitate adding additional pathways into the code. Sometimes those unexpected errors can happen in multiple locations, such as loss of backend connectivity. And sometimes the workflows just change, requiring code changes, and sometimes additional branching if we need to support multiple versions with our test.

 

Every one of these conditions requires additional maintenance work. And that work usually adds new pathways and, in particular, extra complexity to the test code being constructed.

 

However, this is not how a human being responds to software. We get a stimulus from it, such as the appearance of a dialog, we digest that, and come up with an appropriate response out of our experience of the software. Our brains can literally process hundreds of possible responses very quickly to find the right one, if we have broad enough experience with the software.

 

Modeling this kind of approach gives us test software that responds intelligently to the software under test. If the order of steps in a workflow changes, we can deal with it without any code changes. If a new error is detected, we don’t have to add complexity, just add another stimulus/response case.

Assumptions

This project was implemented using HP’s Quick Test Pro, which uses VBScript as it’s main scripting language. One of the limitations of VBScript is it’s lack of true object support. However, for another project, I had built a true object engine called VBScript++, documented elsewhere (TBD). It is assumed that the reader is familiar with QTP, VBScript and most importantly VBScript++.

Overview

The core of this implementation are the paired ideas of events and queues. Each is implemented as a VBScript++ BagClass. The test automation engineer creates groups of these event objects, and places them into queue objects. Queues can contain events, or other queues, or both.

 

Once a queue (or group of queues) is populated, the root most queue is executed. It cycles through all it’s children until a timeout condition is reached, or until one of the children marks the queue as complete.

 

Each event represents a condition to watch for. And if that condition is met, and the event is triggered, then it’s code can respond to the condition that has occurred. To give an example, if you wished to log in to a web site, you’d go through several steps:

 

1)     Launch the browser

2)     Enter the URL

3)     Wait for the login page to be displayed

4)     Enter your user name

5)     Enter your password

6)     Click OK

 

In traditional test automation, if an unexpected message box came up, such as a printer error message, the script would either halt, or it would have to contain numerous checks at different places in case such a window came up. Some testing tools allow you to set “traps” that will respond to simple message boxes by specifying the message box and the button to click, but if your tool lacks that, or if it’s more complex to deal with the dialog than simply pressing a dismiss button, then the script will be unable to respond to it, and the test will fail for reasons completely unrelated to the test.

 

In addition, if the order of the steps changes, then the script must be updated. If there are multiple different orders to the same steps, based on product versions, that all must be supported at the same time, then it gets even more complicated.

 

On the other hand, in the event based system, we can create events that watch for specific conditions and respond to them, regardless of the order they happen in, like so:

 

Table 1, Login Event List

Event Name

Stimuli

Response

Verification

LaunchBrowser

No browser window is displayed

Launch browser

Verify browser is running

NavBrowser

Browser is open but not to login page

Enter login URL

Verify login page is displayed

EnterUser

Login page is displayed, and user name field is blank

Enter user name

Verify user name field has correct value

EnterPass

Login page is displayed, and password field is blank

Enter password

Verify password field has correct value

SubmitLogin

Login page is displayed, and user name and password fields are not blank

Click OK

Verify that browser moves to logged in page

LoggedIn

Logged in page is displayed

Mark this test as passed and this queue as complete

Verification is inherent in the stimuli

LoginError1

Credentials do not match error message is displayed

Fail test, mark queue as complete

Verification is inherent in the stimuli

PrintMsg

Print error message is displayed

Bring print error message to the front

Dismiss print error message

Verify print error message disappears

 

In this case, if the print error is displayed after the browser is launched, and the test is unable to populate the fields, the queue will simply keep going until it gets to the event that dismisses the print error, then it will loop around and start at the top again, picking up where it left off, and completing the login process.

 

As new situations that the automation must deal with occur, we simply add more events to the system. We call this “compartmentalized complexity”. While we are in fact adding more complexity, it’s in a compartmentalized manner, and no event impinges on any other.

Implementation

Let’s Start With Events

The core of the system is the event. Each of the lines in the table above represents one event. In our code, an event is a bagclass, and you’ll create a baginstance for each event. Each event has 3 methods that are the core of event functionality. They are: “HasContextStart”, “Transform”, and “HasContextEnd”.

 

Method:HasContextStart is called during each cycle of the queue, and looks to see if that event’s stimuli exists. So in the example table above, the first entry, named “LaunchBrowser”, the stimuli is “No browser window is displayed”, and Method:HasContextStart will return True if that condition is met. Functions implemented in HasContextStart *do not wait* for a window to appear. It’s either there now, or it’s not. This keeps the whole queue of events moving quickly.

 

Method:Transform is where the work of the event happens. In the case of LaunchBrowser, the transform method will launch the browser.

 

Method:HasContextEnd then will verify that the transform happened correctly. If it did, HasContextEnd will mark the event as complete. If it hasn’t happened correctly, HasContextEnd may throw an error (more on this later), or may take some other action. This is where the bulk of synchronization happens. If we did a transform step, then we wait to see if the window it invoked has appeared in this function. If it doesn’t in a timely manner, this is where we note the error.

 

In addition to the above, queues contain a plethora of other methods and keys, most of which we’ll cover in more detail later. However, there are a couple of items we do need to cover before going on.

 

Object Key:Name – Every event must have a unique name. If you reuse the name, the first instance to use that name will get overwritten by the second event to use that name. Name is usually assigned when the object is instantiated. We’ll create one who’s name is LaunchBrowser, like so:

 

Set temp = ClassEvent.NewObject(“name=>LaunchBrowser”,NULL)

 

Object Key:Complete – this object data item is initially set to False. It is used to show that the event has not successfully completed it’s behavior. As long as Key:Complete is False, that event’s HasContextStart will keep being polled during processing. As soon as it’s marked as complete, HasContextStart will no longer be called. Once the event *is* complete, HasContextEnd would set the event to complete, like so:

 

self(“complete”) = True

 

Method:Attach – Used to attach this event to a queue. Like so:

 

self.Method“attach”, array(“item”,GlobalProcessQueue)

 

I know that all of this is sort of meaningless without some good examples, but before we can get to the examples, we need to know a just a little bit about queues…

A Quick Word On Queues

The “scaffold” the system rests in are bagobjects called queues (implemented as ClassProcessQueue). These are container objects which contain the events to be triggered. Queues can also contain other sub queues, and any data to be shared amongst the items in that queue.

 

The system comes with one queue already instantiated once the file is loaded into QTP. That baginstance is called GlobalProcessQueue. Other queues are generally spawned from the global queue. They can also be generated as stand alone queues for specific needs.

 

The most critical element of queues is Method:Process. This is used to kick off a queue. Once it’s started, it will loop through all the events in the queue that are relevant until either one of the events marks the queue as complete, or until it’s looped a certain number of times with no events triggered (a timeout condition).

Now For An Example

 

Let’s start with the basics. We’re going to create a variable to put our instance in, called temp. It’s called that specifically because in later examples, we are going to reuse the variable.  After that, we’re going to create an event called LaunchBrowser to put into it. (Please note: In QTP, the methods for the events cannot be in the test, they *must* be in a library file.) So we will create an instance, then attach it’s methods to it. After that, we’ll attach the event to the queue, and execute the queue.

 

One quick note on VBScript++, one of the features of this library is that we can attach methods to instances. In some languages, to attach a method to an instance you’d first have to create a subclass with the new method, and then instantiate that. With VBScript++, this isn’t necessary.

 

Now let’s see how this looks in code:

 

Dim temp

Set temp = ClassEvent.MakeObject (“name=>LaunchBrowser”, NULL)

 

Now we take advantage of one of the fact that we can add methods directly to an instance:

 

Function Event_LaunchBrowser_HasContextStart(self, args)

    Event_LaunchBrowser_HasContextStart = Browser().Exist(0)

EndFunction

temp.ApplyMethod“HasContextStart”,“vector=> Event_LaunchBrowser_HasContextStart”

 

Function Event_LaunchBrowser_Transform(self, args)

    Browser().Launch()

EndFunction

temp.ApplyMethod“Transform”,“vector=> Event_LaunchBrowser_Transform”

 

Function Event_LaunchBrowser_HasContextEnd(self, args)

    If Browser(0).Exist(60)Then

        self(“complete”) = True

    EndIf

EndFunction

temp.ApplyMethod“HasContextEnd”,“vector=> Event_LaunchBrowser_HasContextEnd”

 

Having attached all the methods to the event BagObject, we can now attach that to a queue:

 

temp.Method“Attach”, Array(“item”, GlobalProcessQueue)

 

… add additional events to the queue in the same manner here …

 

And finally, we execute the queue:

 

GlobalProcessQueue.Method“Process”, NULL

 

And that’s the most basic setting up and executing an event!

 

In this example, the queue will execute the event, and then loop around for a while until it times out. The reason for this: We haven’t specified a queue ending condition.

 

The queue can be marked complete in the same manner as the event. For instance:

 

Set temp = ClassEvent.MakeObject (“name=>LoggedIn”, NULL)

Function Event_LoggedIn_HasContextStart(self, args)

    Event_LoggedIn_HasContextStart = Browser().Page(“Logged In OK”).Exist(0)

EndFunction

temp.ApplyMethod“HasContextStart”,“vector=> Event_LoggedIn_HasContextStart”

Function Event_LoggedIn_Transform(self, args)

    ‘ this next line marks the current queue complete.

    self(“queue”)(“complete”) = True

EndFunction

temp.ApplyMethod“Transform”,“vector=> Event_LoggedIn_Transform”

Function Event_LoggedIn_HasContextEnd(self, args)

    self(“complete”) = True

    ‘also mark test or test step successfully completed here

EndFunction

temp.ApplyMethod“HasContextEnd”,“vector=> Event_LoggedIn_HasContextEnd”

temp.Method“Attach”, Array(“item”, GlobalProcessQueue)

 

This code adds a new event, one from table 1 above, which detects the successful completion of the event group listed in the table. Which is to say, we’re now logged in successfully. Once that condition has been detected, we mark the queue complete, which halts further event polling.

 

To complete this login procedure, we simply keep adding event BagObjects until we’ve covered all the events in the table.

A Note On This Design

I’ve been doing test automation for a long time, and I’ve noticed a couple of things. An awful lot of folks do not verify each logical test step. They assume that if they populated the field without an error, then the test step completed ok. Which sometimes means the test fails in some location several steps past the actual error.

 

In addition, many folks fall into the trap of writing their test in such a way that the test is assumed to have passed until there is an explicit failure. Best practices in many other technical fields dictate with good reason that a test should be assumed to fail until all the key validations have passed.

 

The reason for 3 key routines in the events is to explicitly separate the validation from the transform. Do the transform in Method:Transform. Then verify that it worked in the Method:HasContextEnd. And only then mark it completed. This helps us adhere to the best practices without having to think too much about the structure for doing it.

Advanced Topics

More Notes About Events And Queues

Making a New Queue

There are two ways to make a new queue. The most common one for simply defining a static group of events in subqueues is like this:

 

Set mySubQueue = GlobalProcessQueue.Method (“Spawn”,  NULL)

 

The other way is used when making stand alone queues:

 

Set myStandAloneQueue = ClassProcessQueue.NewObject(“name=>myQueue”,NULL)

On Speed

The Method:HasContextStart functions get called over and over again. This is how we look for events in this system, because there isn’t a way for actions of the application under test to push events into a queue. So, we model the human response, we look for what we’re familiar with. And we do it by writing code in HasContextStart which basically says “does our world look like this? No? How about this?” and so on. Every one of those could get called hundreds or even thousands of times depending on how we design our queues.

 

Given all these calls, it’s imperative that there are no delays in HasContextStart, nor really anything that isn’t absolutely necessary. If you find yourself thinking of closing a window in a HasContextStart, don’t. Move it to it’s own event.

On Designing HasContextStart

One needs to keep in mind that this is a kind of rules engine. HasContextStart is an embodiment of rules in code. So it’s important to have enough checks in your HasContextStart that you can be sure that it is only responding to the condition you want. So for instance this is not a good idea:

 

Function Event_Foo_HasContextStart(self, args)

       Event_Foo_HasContextStart = Browser().Page(“logged in”).Exist(0)

EndFunction

 

Because your logged in page might well appear right after logging in, but if this event is not marked complete, and you move on to some other page, such as ordering a product, and the logged in page is no longer displayed, this event will be triggered again.

On Referencing Each Other

First off, it’s important to remember that every queue and event must always have a name that is unique within it’s parent queue. Reusing a name will cause an event or queue to be overwritten, as the name is used as the key in the queue storage dictionary.

 

Events can send messages to their queue’s by referencing the key:queue (which gets populated at instantiation). In the example below, we’re going to return any error tags that have been posted to the queue. Remember that all these bagobjects are essentially turbocharged dictionaries, so the event class contains a key:queue, that references the queue bagobject, which in turn contains key:error_tags. We’d reference it like so:

 

t = self(“queue”)(“error_tags”)

 

Queue’s contain a key:data, which is the actual dictionary of items in the queue. If you need to refer to another event in the queue, you’d do that like this:

 

self(“queue”)(“data”)(“otherEventName”).Method“reset”, NULL ‘ resets the event named “otherEventName”

 

Queues can contain queues can contain queues can…. You get the idea. You might have something that looks like this:

 

self(“queue”)(“queue”)(“queue”)(“data”)(“otherEventName”)

 

It’s important to remember that the Key:queue (common to both events and queues) is set *at instantiation time*. Also set at attachment time is the Key:master_queue. It’s set to the *rootmost* queue at the time of attachment. If you create a queue, and populate it, and then attach it to another queue, the Key:master_queue in the contained events will not point at the new rootmost queue. So sometimes we want to do this:

 

Set x = self.Method(“FindMaster”, NULL)

 

And then from there navigate to the item we want.

On Completion

Any event which may be reused, such as the event in table 1 called PrintMsg, which dismisses a print error message, should NOT mark itself complete when done. That way, if it’s condition is met again, it can respond again.

 

At the same time, we can base a HasContextStart on the completion of another event. Like so:

 

Function Event_Foo_HasContextStart(self, args)

       Event_Foo_HasContextStart = self(“queue”)(“data”)(“bar”)(“complete”)AND Dialog(“x”).Exist(0)

EndFunction

 

In addition, there are two ways to mark something completed. One is via key:complete, as described above, the other is via the Method:Complete. Let’s just look at several examples:

 

self(“complete”) = True‘ marks me complete

self.Method“complete”, NULL ‘ marks me complete

 

self(“queue”)(“data”)(“otherEventName”)(“complete”) = True‘ completes event named “otherEventName”

self(“queue”)(“data”)(“otherEventName”).Method“complete”, NULL ‘ completes event named “otherEventName”

 

self(“queue”)(“complete”) = True‘ marks the queue complete, but not it’s children

self(“queue”).Method“complete”, NULL ‘ marks the queue and all it’s children complete

 

An important thing to note about those last two, once the queue is marked complete, it will stop executing, whether all it’s children are marked complete or not.

 

In the same way that there are method:complete for both queues and events, there’s also the complimentary method:reset for both as well, which “uncompletes” them.

On Data

In general, all the data that you’ll be using that is shared between the events in a queue should be stored in the queue itself. For instance, in our login example in table 1 above, we probably will need login credentials. So before we invoke the queue, we would set up the data like so:

 

loginQueue(“username”) = userNamePassedInFromOutside

loginQueue(“password”) = passwordPassedInFromOutside

loginQueue.Method“Process”, NULL

 

And within the events, we’d do something like this:

 

Function Event_EnterUser_Transform(self, args)

    Browser(n).Page(“Login”).WebEdit(“userid”).Set self(“queue”)(“username”)

EndFunction

 

In addition to posting data to the queue from outside, events can post data to each other in their queue object as well.

Errors And Error Handling

There are two kinds of events. Ones for handling errors and ones for regular events. Regular events are NOT polled when an error has been raised, and when an error has been raised, ONLY error handlers are polled.

 

It’s important to note that the error information is stored at the queue level, not the event level. That said, all the *methods* are relevant to both the queue and the event bagobjects.

 

It’s also important to note that there *are no error handling events* in table 1. Those are all normal events. Before we look more in depth at error handling events, let’s create an example. Table 2 below adds events to the queue in Table 1.

 

Table 2, Login Retry

Stimuli

Event Name

Error handler

Transform

Verification

Login provided is already in use error

LoginBusy

No

Throw error with tag loginbusy

None needed

Error raised w tag loginbusy

CredentialUpdate

Yes

Have we used all the available credentials?

YES: Throw new error with tag allloginsbusy

NO: update to next credentials, mark login events not complete, clear error

None needed

Error raised w tag allloginsbusy

AllLoginsBusy

Yes

Log a test abort condition and note in the log that we could not complete the test because all available logins were busy. Mark queue as complete.

None needed

 

Creating Error Handlers

This categorization between normal events and error handling events is controlled by the ClassEvent key:error_handler. To instantiate an error handler, we do this:

 

Set temp = ClassEvent.MakeObject (“name=>CredentialUpdate|error_handler=><Eval>True, NULL)

Throwing An Error

To throw an error, we do this:

 

self.Method“ErrorThrow”,“tags=>loginbusy|description=>userid was already in use”

 

The key:tags can contain any string you wish to use. Common ones include fatal, fail, warn, info, and debug. But any string may be used. Commas are reserved characters, and used as the separator.

 

The key:tags can contain multiple tags, which allows more than one event to respond to an error condition. Once an error has been thrown, any error handler event may watch for that error. In general, a good practice is to use the tags in your HasContextStart condition:

 

Function Event_CredentialUpdate_HasContextStart(self, args)

       Event_CredentialUpdate_HasContextStart = Contains(self(“queue”)(“error_tags”),“loginbusy”)

EndFunction

Reraise

If an error handler determines that the error cannot be handled at this level, it can reraise the error, bouncing it up to the next queue in the tree. This is done like so:

 

self.Method“ErrorReraise”, NULL

 

This causes the whole queue, and all it’s events, to be marked complete.

 

If the error is reraised all the way to the root, and no handler intercepts it, then a fatal error is raised by the root queue.

Clearing An Error

Once an error handler has successfully *cleared* an error, it can use Method:ErrorClear to clear the error state:

 

self.Method“ErrorClear”, NULL

 

Any error handler which is *self contained*, which is to say, can clear the error on it’s own, and the error is one that may recur again; should not mark itself complete when it’s done.

Error Handlers Resetting Completed Events

In general, if any error handler creates a condition where some steps that had already been done should be repeated, we use the Method:Reset to clear the completed state.  Depending on whether you’re resetting the current event, another event, or the whole queue, your code will look something like this:

 

self.Method“reset”, NULL ‘ resets the current event

self(“queue”).Method“reset”, NULL ‘ resets the queue

self(“queue”)(“data”)(“otherEventName”).Method“reset”, NULL ‘ resets the event named “otherEventName”

 

Detailed Queue Processing

One of the last pieces of complexity around this model is how queues are processed. There are a number of parameters that control execution of a queue. Most of them are detailed later, but there’s one aspect that merits it’s own section. And that’s how queues are processed.

 

The most obvious way to process events is to simply put them all in one queue and run through them again and again until they’re all done with their workflow, and that’s what we’ve looked at so far. However, there are a number of other possibilities:

The External Called Queue

Sometimes you might notice something happen, and need to take a complex action in response to it, but don’t want to necessarily put all your events in your main queue. One way to do this is to create a second queue, and call it from within one of the first queue’s events. When you do this, processing on your main queue stops, and your external called queue executes to completion, at which time execution picks up on the next line of your event. Like so:

 

Function Event_LoginToVPN_Transform(self, args)

    VPNLoginQueue.Method “Process”,Array(“username”,self(“queue”)(“username”),_

                  “password”,self(“queue”)(“password”))

EndFunction

 

This will cause the VPNLoginQueue to be executed and completed before resuming execution on the main queue. This allows us to break up our test code into discrete, reusable components.

Serial And Parallel

If you’re only using one queue, then each event will be polled, then it’s successor, and so on until all the queue is either complete or has timed out.

 

But sometimes, you need to have finer control than that. An example might be if you wanted to watch for a range of possible responses from an application, which aren’t determinable in advance. These might be based on real world impacts on the test environment, for instance.

 

So the way we manage that is to have different queue execution modes. These modes are called “serial” and “parallel”. The default is parallel. This mode selection is controlled by the queue object key:stepping_type. Let’s look at an example:

 

Root queue                queue 1

                                                Event 1.1

                                                Event 1.2

                                                Event 1.3

                                    queue 2

                                                Event 2.1

                                                Event 2.2

                                                Event 2.3

 

So if everything is set to it’s defaults, and we tell the root queue to execute itself, the *events* will be processed in this order:

 

            Event 1.1

            Event 2.1

            Event 1.2

            Event 2.2

            Event 1.3

            Event 2.3

 

So the first sub-queue gets one processing cycle (though the exact number is controllable, more on that later), and then the second one. And so on.

 

Now let’s take a different example. Let’s say you want to run through a series of checks for conditions not related to your test, after each test related event. To do this, using the example above, we’ll change the behavior of queue2:

 

MasterQueue(“data”)(“queue2″)(“stepping_type”) = “serial”

 

Now the execution will look like this:

 

            Event 1.1

            Event 2.1

            Event 2.2

            Event 2.3

            Event 1.2

            Event 2.1

            Event 2.2

            Event 2.3

            Event 1.3

            Event 2.1

            Event 2.2

            Event 2.3

 

So after one event from queue1, all the events from queue2 are run. And then we go back to queue1 for it’s next event.

Tips, Tricks And Warnings

Loops

We can build loops into the system as well. We could have an event which does the following psudocode:

 

HasContextStart

            If last event is done OR queue variables are empty

Transform

            Read data from spreadsheet

            If we’re at end, mark queue complete

            Else populate the queue variables

            And reset all the events

HasContextEnd

            Do nothing!

 

And that will let us implement a data set test that runs in a loop until it runs out of data. Note that this event never marks itself complete. It doesn’t need to.

 

Classes

One of the possible useful things to do with this framework is build event templates. You can create a new subclass of event that has all the methods defined in it. You can define the queue based subclass to “build out” it’s list of events based on data passed to the queue. Then you instantiate this class, give it it’s data, and run it. Looking at our login example again, I’ve created an example below. let’s say we did it this way:

 

Dim ClassMyLoginQueue

Set ClassMyLoginQueue = classBase.MakeClass(“ClassMyLoginQueue”,NULL,“self.is_virtual=><eval>False”)

Function ClassMyLoginQueue_BuildAndRun(self, args)

       ‘ you should check here to see that the queue has the correct data keys

       ‘ and report out however you want to design it of that failure case

       ‘ in our case, it’s the username and password fields

       ‘ with that said, on with the show…

      

       Dim results

       Set results = BagDictMake(NULL,NULL) ‘ to put results in

       reults(“returncode”) = 0

      

       Dim temp

       Set temp = ClassEvent.MakeObject (“name=>LaunchBrowser”, NULL)

       temp.ApplyMethod “HasContextStart”,“vector=>Event_LaunchBrowser_HasContextStart”

       temp.ApplyMethod “Transform”,“vector=> Event_LaunchBrowser_Transform”

       temp.ApplyMethod “HasContextEnd”,“vector=> Event_LaunchBrowser_HasContextEnd”

       self.Method “Attach”, Array(“item”, temp)

       ‘ now i’m not going to list every event in this example. We still have to have defined

       ‘ the methods, but we reuse the same ones for every instance of this ClassMyLoginQueue

       ‘ so you just imagine all the rest of the events being defined and added here

       ‘ and we’ll move on to the next part of the game

      

       self.Method “process”, NULL

       ‘ and more code here to copy your result information into the result object

      

       Set ClassMyLoginQueue_BuildAndRun = results

EndFunction

 

Dim myInstance, result

Set myInstance = ClassMyLoginQueue.MakeObject(“name=>LoginAttempt1″, NULL)

myInstance(“username”)=“bob”

myInstance(“password”)=“foo”

Set result = myInstance.Method(“BuildAndRun”,NULL)

 

One could do this so many other ways as well. We could flesh out the events in the constructor, instead of this method, we could verify the login keys and do the run in this method. Or even do everything in the constructor, which would give us a “throw away instance” of the queue, just keeping the result data, like so:

 

Dim result

Set result = ClassMyLoginQueue.MakeObject_

(“name=>LoginAttempt1|username=>bob|password=>foo”, NULL)(constructor.result)

Don’t Reuse Event Instances In Multiple Queues

If you need to reuse an event in multiple queues, create a class, and create multiple instances of the class. Each event carries a key:queue that points to the queue it’s attached to. You can attach it to multiple queues, but if you do, it will overwrite this key, and you’ll lose the pointer to the original queue.

Complete Reference

Alpha Order

ClassEvent Key:Completed

Returns boolean of “is this event completed”. The initial value is set in the constructor.

ClassEvent Key:Enabled

Returns boolean of “is this event enabled”. The initial value is set in the constructor.

ClassEvent Key:Error_handler

Non error events are executed as long as the error flag is False, which is the default. Only error events are executed if the error flag is set True.

ClassEvent Key:Name

The name of this event, must be unique.

ClassEvent Method:Attach

Attaches the specified item to the current queue.

ClassEvent Method:Complete

Marks this event as complete.

ClassEvent Method:Constructor

Initializes the instance.

ClassEvent Method:ErrorClear

Clears the error flags.

ClassEvent Method:ErrorReraise

Marks this queue complete, and bounces the error up to the next queue.

ClassEvent Method:ErrorThrow

Sets the error flags.

ClassEvent Method:FindMaster

Returns the current highest level master queue (as distinct from the Key:master_queue which is the master as of attachment time).

ClassEvent Method:HasContextEnd

Verifies that Transform was successful and marks this event complete if that’s what’s appropriate (this stub is intended to be overwritten).

ClassEvent Method:HasContextStart

Checks to see if the start condition exists (this stub is intended to be overwritten).

ClassEvent Method:Process_Items

Used internally. Processes this event.

ClassEvent Method:QueuePingRecieverForContextStart

Used internally, this is where the determination is made about whether to call HasContextStart based on key:enabled key:complete and queue key:error_flag.

ClassEvent Method:Reset

Marks this event as NOT complete

ClassEvent Method:Transform

Does the event action (this stub is intended to be overwritten).

ClassProcessQueue Key:Complete

Flag created by constructor which flags this event as complete. Default is False.

ClassProcessQueue Key:Enabled

Flag created by constructor which flags this event as enabled. Default is True.

ClassProcessQueue Key:Error_flag

Boolean indicator of an error having been thrown.

ClassProcessQueue Key:Error_tags

Descriptive tags about the error for other events to process.

ClassProcessQueue Key:Error_description

Descriptive message about the error. Intended for human reader.

ClassProcessQueue Key:Item_counter

The current record being processed in the queue.

ClassProcessQueue Key:Last_item_counter_checked

The index number of the last item executed.

ClassProcessQueue Key:Last_item_name

The name of the last event executed.

ClassProcessQueue Key:Loops_before_halting

The number of loops that can occur with no actions having been taken before the processing halts.

ClassProcessQueue Key:Loops_since_last_triggering

The number of times the entire set has executed since the last event found a triggering stimuli.

ClassProcessQueue Key:Master_queue

Set by Method:Attach and Method:Spawn, sets the root owning queue *at the time of attach*.

ClassProcessQueue Key:Name

The name of the queue.

ClassProcessQueue Key:Queue

Set by Method:Attach and Method:Spawn, points to the owning queue.

ClassProcessQueue Key:Seconds_to_pause_at_end

Default zero. Number of seconds to delay at the end of each queue before running the first event again.

ClassProcessQueue Key:Stepping_length

The number of items to be processed during each “interval”. Default is 1.

ClassProcessQueue Key:Stepping_type

String of “serial” or “parallel”, refers to how the queue will be processed at each “process” method call (referred to as an “interval”). Can process the number of events  specified in “stepping_length” when set to parallel, and will process all the events in the queue once if set to “serial”

ClassProcessQueue Method:Attach

Attaches the specified item to the current queue.

ClassProcessQueue Method:Complete

Marks all the items attached to this queue as complete.

ClassProcessQueue Method:Constructor

Sets up the queue item, key:complete, and key:enabled.

ClassProcessQueue Method:ErrorClear

Clears the error flags.

ClassProcessQueue Method:ErrorReraise

Marks this queue complete, and bounces the error up to the next queue.

ClassProcessQueue Method:ErrorThrow

Sets the error flags.

ClassProcessQueue Method:FindMaster

Returns the current highest level master queue (as distinct from the Key:master_queue which is the master as of attachment time).

ClassProcessQueue Method:Process

Executes this queue.

ClassProcessQueue Method:Process_Items

Processes a subset of the items attached to this queue  (how many items is set by the key:stepping_length and by the key:stepping_type).

ClassProcessQueue Method:Reset

Marks all the items attached to this queue as NOT complete.

ClassProcessQueue Method:Spawn

Creates a new queue which is attached to this queue.

Summary

This framework component lets me build workflows that are not order dependent. It also lets me componentize parts of workflows in reusable ways. It’s an idea I’ve been thinking about for a very long time, and am glad I finally got the chance to build it.

 

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.

·    vbs++_ActionsAndEvents.vbs: The event manager library, defining the event and processqueue classes.

,

VBScript++

0
by on January 11, 2010 at 11:42

Akien MacIain, Senior Automation Engineer,
Delta Dental of California


Table Of Contents

Introduction. 2

Overview. 2

The BagObject Framework. 4

Creating new BagClasses. 4

Adding Methods to a BagClass. 4

Adding Properties to a BagClass. 5

Creating BagInstances of a BagClass. 6

Calling methods and properties. 6

How it’s most commonly used. 6

Custom Signatures. 7

Constructors. 8

Debugging. 8

Other little amusing oddities. 8

Summary. 8

Attachments. 8

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”)

EndFunction

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”))

   EndIf

EndFunction

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”))

EndFunction

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:

, ,

Overriding QTP’s native reporter

1
by on July 7, 2009 at 19:59

Background

Over the years, we’ve reviewed and discussed a wide and varied array of replacements and enhancements to QTP’s native reporter: From reporting an hierarchical structure, through parallel export to an HTML file, and all the way up to an overall reporting framework.

All these solutions and workarounds work wonderfully in case you’re just starting to build your automation library, but they are hard to implement in existing projects. If you already have a lot of code that uses the Reporter.ReportEvent command directly, you’re in for a lot of editing and code-tweaking in order to make you code work with the new reporting solution.

If you’re stuck with the latter case, I hope this article will help you implement whichever solution you’d like much more easily. However, you should also learn a valuable lesson from this: Always wrap your common commands in functions. If you had a Report function, you could just change its implementation and have your new reporting solution working in no time.

The Implementation

So, how can you override the native Repoter.Reportevent command? We’ll build a mock Reporter class, and steal QTP’s native reporter place.

First, let’s build our mock class. Basically, it’s a simple class with a single ReportEvent method:

<span style="color: #0000ff">Class</span> clsReporter
    <span style="color: #0000ff">Dim</span> oFileReporter
 
    <span style="color: #0000ff">Public</span> <span style="color: #0000ff">Sub</span> ReportEvent (iStatus, sStepName, sDetails)
        <span style="color: #008000">\'Enter the relevant code for your reporting solution</span>
        <span style="color: #008000">\'For example, this reports the event to a text file</span>
        
       oFileReporter.AppendAllText(<span style="color: #006080">"C:\Log.txt"</span>, sStep Name & <span style="color: #006080">" - "</span> & sDetails & vbcrlf)
    <span style="color: #0000ff">End</span> <span style="color: #0000ff">Sub</span>
 
    <span style="color: #0000ff">Private</span> <span style="color: #0000ff">Sub</span> Class_Initialize
        <span style="color: #0000ff">Set</span> oFileReporter = DotNetFactory(<span style="color: #006080">"System.IO.File"</span>)
    <span style="color: #0000ff">End</span> <span style="color: #0000ff">Sub</span>
<span style="color: #0000ff">End</span> Class

This specific class reports to a text file, but the ReportEvent sub can do anything you’d like it to do – call ReporterManager, report to excel, print out message boxes, or do all of these at once.

Now, for the important part – in order to override QTP’s native reporter, place this class in an external library file, and attach it to your test. Then, add the following code at the top of the file (outside the class):

<span style="color: #0000ff">Public</span> Reporter
 
<span style="color: #0000ff">Set</span> Reporter = <span style="color: #0000ff">New</span> clsReporter

This will effectively cause all Reporter.ReportEvent commands in library files to funnel back to our custom class, and execute our very own report implementation.

An overall solution

As noted above, this only works for reporter calls made in external library files. QTP’s script management makes our little trick take precedent over QTP objecta in the external libraries, but it doesn’t work for code written directly within actions. In order to override the Reporter object within the actions, we’ll have to add the following function to our external library file, just above the class definition:

<span style="color: #0000ff">Function</span> GetReporter
    <span style="color: #0000ff">Set</span> GetReporter = Reporter
<span style="color: #0000ff">End</span> Function

And use the following code as the topmost command in each of our actions:

<span style="color: #0000ff">Set</span> Reporter = GetReporter

This will cause the following chain reaction:

1. When the test begins to run, the external library files are executed. This causes our custom class to override QTP’s native reporter.

2. When a test enters an action, it create a new script “bubble”, and in that bubble, re-overrides our custom reporter with QTP’s native reporter.

3. In order to fix the bubble, we run a command within the action, which fetches the custom reporter from the library file (via the GetReporter funciton), and overrides the local reporter object with in.

4. Now Reporter.ReportEvent commands performed within the action will funnel to our custom reporter, even though it was defined in an external file.

Notice however, that this workaround have a major flaw – what if you still want to report to the native reporter (in parallel to your own custom reporter)? we can’t simply call the Reporter.ReportEvent command – we’ve just spent a lot of effort to override it!

There is a partial workaround for this problem as well, but it’s way more complex than this article’s scope.

Update: hsupadrasta has come up with a nice simple workaround for the above problem.

Just add this code on at the top of your actions:

<span style="color: #008000">\'Save the native reporter to a global variable</span>
ExecuteGlobal <span style="color: #006080">"Dim oNativeReporter"</span>
<span style="color: #0000ff">Set</span> oNativeReporter = Reporter
 
<span style="color: #008000">\'Now we can overrun the reproter with our class</span>
Execute <span style="color: #006080">"Dim Reporter"</span>
<span style="color: #0000ff">Set</span> Reporter = GetReporter

 

And now your could still use the native reporter via the oNativeReporter variable.

Thanks hsupadrasta for this great tweak!

Putting it all together

Since I’ve explained the different components of the workaround separately, it may come across as overly complex, but in fact, it’s quite simple. Here’s the entire code combined (goes within an external code library):

<span style="color: #008000">\'Override it with our custom class</span>
<span style="color: #0000ff">Dim</span> Reporter
<span style="color: #0000ff">Set</span> Reporter = <span style="color: #0000ff">New</span> clsReporter
 
<span style="color: #008000">\'Define a funnel function to be called from the test actions</span>
<span style="color: #0000ff">Public</span> <span style="color: #0000ff">Function</span> GetReporter
    <span style="color: #0000ff">Set</span> GetReporter = Reporter
<span style="color: #0000ff">End</span> <span style="color: #0000ff">Function</span>
 
<span style="color: #008000">\'Class definition</span>
<span style="color: #008000">\'In the example, our class just reporter to a text file</span>
<span style="color: #0000ff">Class</span> clsReporter
    <span style="color: #0000ff">Dim</span> oFileReporter
   
    <span style="color: #0000ff">Public</span> <span style="color: #0000ff">Sub</span> ReportEvent(iStatus, sStepName, sDetails)
        oFileReporter.AppendAllText <span style="color: #006080">"c:\log.txt"</span>, sStepName & <span style="color: #006080">" - "</span> & sDetails & vbcrlf
    <span style="color: #0000ff">End</span> <span style="color: #0000ff">Sub</span>
 
    <span style="color: #0000ff">Private</span> <span style="color: #0000ff">Sub</span> Class_Initialize
       <span style="color: #0000ff">Set</span> oFIleReporter = DotNetFactory(<span style="color: #006080">"System.IO.File"</span>)
    <span style="color: #0000ff">End</span> <span style="color: #0000ff">Sub</span>
<span style="color: #0000ff">End</span> <span style="color: #0000ff">Class</span>
 

And this code at the top of each action:

<span style="color: #0000ff">Dim</span> Reporter
<span style="color: #0000ff">Set</span> Reporter = GetReporter

 

I hope this would help you make a smoother transition into your new reporting framework.

in Articles, Yaron Assa

, ,

Implementing a GUI Layer with Classes (Russian Translation)

0
by on May 11, 2009 at 13:44

Sergey Talalaev has put his time and effort into translating Meir Bar-Tal’s article on Implementing a GUI layer with classes into Russian. We full-heartily thank Sergey on his continued help and support to the QTP community!

Аннотация

Эта статья описывает действенную технику, которая используя ООП шаблоны проектирования, дескрипторное программирование (DP) и объект Dictionary позволяет объединить GUI объекты вместе с их бизнес функциями. Статья также включает ценное дополнение: эффективный прием, позволяющий избежать зависания QTP при попытке обращения к несуществующим GUI объектам во время выполнения.

Введение

Основная проблема в автоматизированном тестировании – это как уменьшить издержки по поддержке скриптов. Вопросы вида: “Должны ли мы использовать объектный репозиторий (Object Repository OR) или дескрипторное программирование (Descriptive Programming DP)? Если выбран OR, то должны ли мы использовать общий OR или локальный для каждого Action? Если выбран DP, тогда какой оптимальный вариант его реализации?” достаточно повсеместны и ответы на них могут зависеть как от конкретных особенностей проекта так, во многих случаях, и от людей, вовлеченных в процесс.

В этой статье я проанализирую концепцию Тестовых Объектов (Test Objects) в рамках принципов OO (Object Oriented). Я попытаюсь показать, что реализация объектного репозитория в QTP не соответствует этим принципам, и каково влияние этого факта на разработку автоматизированных тестов с точки зрения эффективности затрат и трудоемкости поддержки. Затем я опишу расширение OR-концепции, которое соответствует принципам OO – GUI слой (GUI Layer). Эта концепция была адаптирована экспертами компании SOLMAR, основываясь на том факте, что возможно максимизировать повторное использование кода (и таким образом улучшить поддерживаемость кода) используя OO подход, согласно которому автоматизируемый проект разбивается на несколько уровней абстракции.

 

Тестовые Объекты и Run-Time Объекты

Тестовый объект содержит в себе ссылку на run-time объект – объект реального мира, который инициирован в тестовом приложении. Тестовый объект содержит описание объекта, на который он ссылается, в виде набора атрибутов и значений служащих критерием для поиска нужного объекта в ходе тестового прогона. Это описание больше всего похоже на то, что вы себе представляете, идя на встречу с незнакомкой (“Я брюнетка, стройная и высокая, и буду одета в красное платье и туфли”)

Аналогично QTP использует описание для запроса к OS, загружен ли в настоящий момент объект в оперативную память. Если ответ положительный, то OS вернет адрес объекта (ссылку или указатель) и, следовательно, QTP получит доступ к публичным методам и свойствам объекта, в то время как частные методы и свойства остаются скрытыми от просмотра по определению (таким образом QTP никогда не вмешивается в частную область, чего вы бы хотели в отношении брюнетки, если повезет!)

Хотя это описание может быть слишком упрощено, но по существу это такой процесс, который позволяет нам взаимодействовать с GUI (и другими) объектами используя QTP или другое средство автоматизации (Test Partner, Test Complete, Rational Robot, AutoIt и т.д.) Более детальное объяснение различия между Тестовыми Объектами и Run-Time Объектами доступно в статье Yaron Assa “Differences and Connections between Runtime Objects and Test Objects” (2009).

 

Тестовые Объекты и Объектный Репозиторий

Рассказанное выше естественно заставляет нас думать о тестовых объектах как о данных. Что я имею в виду? Основываясь на описании, построенном из свойств и значений, QTP имеет возможность распознать GUI объект среди всех объектов, загруженных в данный момент в машинную память. Этот процесс подобен тому, как мы получаем необходимую запись из БД согласно условию WHERE в SQL запросе. Разница лишь в том, что в данном случае OS (Windows), а не БД возвращает запись. Кроме того, запись содержит только одно поле – ссылку на реальный run-time объект. Следуя данной мысли, это кажется вполне естественным хранить эти записи – тестовые объекты (Test Objects) – в базе данных, которой собственно Объектный Pепозиторий (Object Repository) и является (если вы обратили внимание на файлы с расширением bdb в QTP тестах, то знайте, что bdb в действительности означает Berkeley Data Base – продукт компании Oracle).

В чем же собственно проблема с подходом, использующим OR? Тестовые объекты в действительности являются данными, но мы до текущего момента вынуждены выполнять действия с Run-time объектами, что, учитывая вышесказанное, предоставляет нам искаженную картину. Ниже я объясню почему.

Это действительно, правда, что OR, при правильном использовании, предоставляет наилучшую технику, в соответствии с которой нам необходимо сохранить описание каждого GUI объекта всего лишь раз для уменьшения стоимости изменений. Тем не менее, чтобы достичь этого должны быть потрачены большие усилия на правильное управление тестовым проектом, потому что непродуманные запросы и недостаток ресурсов могут привести нарушению целостности этого ценного ресурса (OR). Например, предположим, что два инженера автоматизации работают одновременно с общим репозиторием, но один из них модифицирует скрипт для прошлой версии приложения, а другой – для новой. При условии, что GUI изменения выполняются регулярно от одной версии приложения к другой, не придется долго ждать, пока репозиторий окажется заполненным новыми лишними объектами необходимыми для работы с новой версией приложения. И это еще относительно неплохой результат. Гораздо более худший результат вы получите в случае, если каждый инженер изменяет набор свойств существующих объектов для отражения своих требований, безвозвратно разрушая ROI (возврат инвестиций) проекта автоматизации в целом. Конечно имеются решения и для таких ситуаций, такие как хранение отдельных версий репозитория для соответствующей версии приложения и использование специального ПО по управлению конфигурациями в рамках проекта автоматизации. Но, как говорилось ранее, это требует хорошего управления проектом, что не всегда доступно.

Кроме того поддержка версионности объектного репозитория оставляет открытой одну проблему. По причине того, что автоматические тесты являются регрессионными по своей сути, их модификация затрагивает не только OR, но также и код, который реализует непосредственные манипуляции с GUI объектами и проверки, также как и работу с входными данными и ожидаемым результатом. В общем случае, когда при переходе от одной версии приложения к другой меняются только описания объектов, использование объектного репозитория действительно будет оправданным. Тем не менее, чаще ситуация обратная, изменения в GUI отражают изменения в функциональности приложения, которые в свою очередь гораздо чаще сопровождаются более глубокими изменениями – на уровне БД, например. Таким образом, изменения в GUI требуют изменений как на уровне объектного репозитория, так и на уровне скрипта, который ссылается на тестовые объекты и плюс к этому на уровне входных данных для самого скрипта. Учитывая то факт, что скрипты чаще всего подвергаются модификации, а не разработке, вышеупомянутый анализ приводит меня к мысли, что нечто в концепции OR неправильно по отношении к управлению крупномасштабными проектами автоматизации.

 

Объектно-ориентированный взгляд

Концепция ООП базируется на трех основных принципах – инкапсуляции, наследовании и полиморфизм. Инкапсуляция означает упаковку вместе функциональности и данных, которые сосуществуют вместе и данная упаковка, через которую осуществляются обращения, называется классом. Фактически классы – это представление данных (например, Customer, Product и т.д.) Различие между полнофункциональным классом и обычной структурой данных в том, что класс содержит в себе функции по работе со своими данными – полями. Эти функции обычно называются методами класса. Например, типичный класс для Customer-а включал бы себя следующие поля, которые однозначно определяют покупателя (customerId, firstName, lastName, phoneNo и т.д.) вместе с методами setCustomerId, getCustomerId, которые присваивают и возвращают значения соответствующим полям, также как и getCustomerAge and getCustomerBalance выполняющими расчеты на основе текущих значений полей и возвращающие результат.

Наследование тесно связано с одной из важнейших целей ОО подхода – повторного использование кода. В объектно-ориентированных языках, таких как C++ и Java, повторное использование кода вступает в игру посредством возможности создавать новый класс, который “наследует” поля и методы уже созданного класса (называемого базовым классом) и расширяет их согласно специфическим требованиям, поставленным для нового класса.

Полиморфизм – это еще один мощный принцип, который делает возможным определять несколько версий одной функции (с одним именем, но с различным набором аргументов) для того чтобы прозрачно реагировать на различные ситуации в рамках контекста приложения. Например, у нас есть необходимость производить одну и ту же операцию с различным набором аргументов (float, int) и тогда вместо того, чтобы в одной функции реализовывать набор условных операторов для проверки типов переданных параметров, мы определим несколько функций с одинаковым именем, но с разной сигнатурой (различным набором аргументов). Следовательно, в нашем коде, использующем эти функции, нам не придется использовать приведение типов; мы будем использовать одинаковый интерфейс в обоих случаях, полагаясь на то, что корректный вызов будет осуществлен самой средой выполнения. Тем не менее, в QTP две последние концепции (наследование и полиморфизм) не могут быть реализованы в рамках VB скрипта, который обеспечивает только ограниченную поддержку работы с классами. Тем не менее, позже мы увидим, что это не причина отказываться от использования классов в автоматизации тестирования.

Публичная и частная нотации, упомянутые выше, имеют большое значение в контексте инкапсуляции и наследования. Они позволяют разработчику определять, какие поля (называемые также свойствами или атрибутами класса) и методы будут доступны извне класса (публичные) и какие останутся для внутреннего (частные) использования внутри класса. ОО методология не нова и широко представлена в практике дизайна и разработки ПО. Она общепризнанна как подход, позволяющий сделать код более реиспользуемым, читаемым, поддерживаемым, масштабируемым, расширяемым и конечно более тестируемым (так как большие куски функциональности разбиты на маленькие, самодостаточные пакеты). Конечно, грамотное использование методологии подразумевает глубокие аналитические навыки, позволяющие дизайнеру делать корректные выводы, на основании требований к ПО, о том, какие объекты вовлекаются и как они взаимодействуют между собой.

Как это относится к подходу, который мы выбрали или собираемся выбрать для решения задач автоматизации? Давайте разберемся в этом дальше.

 

Автоматизация Тестирования и Разработка ПО

Я был свидетелем одной вещи на протяжении всей моей карьеры – это то, что QA профессионалы всех уровней рассматривают автоматизацию тестирования лишь как особый вид тестовой деятельности. Не удивительно, что не так уж редко можно встретить профессионалов в автоматизации, думающих также о роли автоматизации. Иногда это даже отражается в названии должностей для персонала автоматизации: “авто-тестер” – это одно из абсурдных названий из тех, что я встретил несколько лет назад. Все это отражает непонимание роли Инженера Автоматизации и, как я объясню в дальнейшем, данное мнение действительно не соответствует сущности автоматизации тестирования.

Задача автоматизации тестирования не должна рассматриваться иначе, чем любая другая контекстно-зависимая задача автоматизации. Вообще говоря, компьютерная программа, которая выполняет набор операций вместо человека – реализует автоматизацию. Таким образом, любой блок кода по сути можно рассматривать как автоматическое устройство или робота. Скрипты, которые производят операции над GUI объектами (как тесты в QTP) не отличаются от других частей ПО. Говоря другими словами, проект тестовой автоматизации, несомненно, является специфичным видом проекта по разработке ПО. И как любой программный продукт проект автоматизации также имеет свой собственный документ с функциональными требованиями (Software Requirements Specifications) и дизайн (Software Test Design) документ (либо тест дизайн в одном из промышленных фреймворков, например HP’s Quality Center или Orcanos’ QPack), которыми должен руководствоваться инженер автоматизации при реализации требуемого кода.

Если это так, то проект автоматизации фактически должен разрабатываться и управляться как любой другой проект разработки, даже более того. А все потому, что инженер автоматизации сталкивается с задачами, которые обычно не касаются разработчиков. Некоторые основные задачи таковы:

  1. Во-первых, инженер автоматизации должен иметь общее понимание системы, так как автоматически скрипт может покрывать широкий спектр функциональности приложения. Члены команды разработки напротив не нуждаются в этом в силу разделения труда среди разных команд обычно координируемых тим-лидерами и менеджером проекта. Таким образом разработчик может сфокусироваться только на часть функционала,за которую он отвечает даже не имеяглубокого понимания системы целиком.
  2. Во-вторых, автоматический скрипт должен быть привязан к тест-дизайну, так как обязан эмулировать действия тестировщика-человека. Но довольно часто инженеры автоматизации обнаруживают, что в тест-дизайне присутсвует множество проблем требующих предварительно выяснения, так как автоматичесикй скрипт не настолько гибок и изобретателен как тестировщик при прогоне тестов.
  3. В-третьих, GUI-элементы используемые командой разаработки и само поведение приложения могут поставить действительно сложные технологические проблемы касающиеся идентификации объектов на уровне QTP (или другого фреймворка). В большинстве случаев ребуются решения, расширяющие базовые возможности фреймворка особенно в случае использования сторонних и собственных комопонентов.

Следуя описанному выше, я думаю, мы можем заключить, что проект автоматизации определенно должен управляться также как проект разработки. Если это так, почему кто-то должен снова и снова становиться жертвой заблуждения о проекте автоматизации как о тривиальной задаче (методика ”записи-воспроизведения” ничего не напоминает?) в то время как верно обратное? Почему бы тогда не использовать широко применяемый при разработке подход – Объектно Ориентированное Программирование – чтобы достичь наилучших результатов? Далее я опишу методику реализации автоматизированных скриптов, основанную на расширении концепции OR ( GUI слой ) и базирующую на твердых принципах ООП.

 

Концепция слоев

Я надеюсь, что объяснил достаточно подробно, почему подход с использованием Объектного Репозитория (OR), повсеместно используемый в скриптах, далек от оптимального. Теперь, продолжая начатое обсуждение, позвольте взглянуть на концепцию разработки кода, разделенного на слои или собственно концепцию GUI слоя. Вообще, слои весьма полезны для максимизации повторного использования кода (возвращаясь к предыдущему обсуждению принципов ООП). Я бы определил GUI слой как множество классов, которые объединяют (инкапсулируют) вместе необходимые интерфейсы для манипуляции GUI объектами тестового приложения для выделенной функционально части. Другими словами – это множество классов, обеспечивающих связь между GUI тестового приложения (то есть определенных тестовых объектов) и Бизнес или Прикладным Слоем, о котором мы поговорим позднее. Может быть, было бы более подходящим назвать его Слой GUI-Бизнес Адаптера, но среди экспертов уже устоялся термин GUI Слой, для краткости. Я проиллюстрирую ниже, как такой слой может быть создан и, какие преимущества достигаются при применении данного подхода в рамках тестовой автоматизации.

 

Реализация GUI Слоя

Инкапсуляция Тестовых Объектов в Классы

Давайте возьмем проект тестовой автоматизации для типичного приложения и посмотрим, каким образом должен быть построен фреймворк для достижения описанных выше целей. Первым шагом будет создание списка всего GUI контекста в приложении – окон (страниц в Web приложениях), диалогов и всплывающих окон. Для каждого элемента этого списка, который в свою очередь выступает контейнером для других GUI объектов, мы определяем класс, например:

<span style="color: #0000ff;">Class</span> Login
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> MainWindow
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> CreateCustomer
<span style="color: #0000ff;">End</span> Class

и так далее для каждого контекста приложения. По причине того, что QTP не позволяет прямое создание класса определенного во внешней библиотеке с помощью оператора New, нам также необходимо определить следующую функцию (разновидность конструктора), которая вернет нам экземпляр GUI-слоя:

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> CreateLogin()
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'Function: CreateLogin</span>
<span style="color: #008000;">\'Creates an instance of the Login class</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' Object - As Login</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #0000ff;">Dim</span> objLogin

    <span style="color: #0000ff;">Set</span> objLogin = <span style="color: #0000ff;">New</span> Login

    <span style="color: #0000ff;">Set</span> CreateLogin = objLogin
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'——————————————————————————-</span>

Второй шаг, очевидно, определить список элементов внутри каждого класса. Теперь, так как каждый класс, согласно вышеизложенному – это контейнер других GUI объектов, мы будем использовать Scripting.Dictionary для хранения ссылок на тест-объекты находящиеся на окне, диалоге или странице. (объект Dictionary широко обсуждался в статьях опубликованных в AdvancedQTP’s базе знаний). Таким образом, первый элемент который я представлю здесь, будет общим для всех GUI Layer классов, и я определю его как m_htChildObjects:

<span style="color: #0000ff;">Class</span> Login
    <span style="color: #0000ff;">Private</span> m_htChildObjects <span style="color: #008000;">\'As Scripting.Dictionary</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> MainWindow
    <span style="color: #0000ff;">Private</span> m_htChildObjects <span style="color: #008000;">\'As Scripting.Dictionary</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

и так далее для каждого контекста приложения (ht – префикс для HashTable, чем объект Dictionary на самом деле и является). Частный элемент m_htChildObjects будет доступен через свойство класса ChildObjects. Это свойство определено как:

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'Property: ChildObjects</span>
<span style="color: #008000;">\'Get and Set the m_htChildObjects member field</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' R/W</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' dic</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' m_htChildObjects As HashTable</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Get</span> ChildObjects()
<span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #0000ff;">Set</span> ChildObjects = m_htChildObjects
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>
<span style="color: #008000;">\'——————————————————————————-</span>

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Let</span> ChildObjects(<span style="color: #0000ff;">ByRef</span> dic)
<span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #0000ff;">Set</span> m_htChildObjects = dic
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>
<span style="color: #008000;">\'——————————————————————————-</span>

Третий шаг – определение всех объектов внутри каждого контекста. Для этой цели я определю публичный метод Init:

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Init()
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'Function: Init</span>
<span style="color: #008000;">\'Initializes the context and child objects</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Dependencies:</span>
<span style="color: #008000;">\' IsContextLoaded(htContext)</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' True/False</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'——————————————————————————-</span>
    ChildObjects = CreateObject(<span style="color: #006080;">"Scripting.Dictionary"</span>)

    <span style="color: #0000ff;">With</span> ChildObjects
        .Add <span style="color: #006080;">"Browser"</span>, Browser(<span style="color: #006080;">"name:=My App"</span>)
        .Add <span style="color: #006080;">"Page"</span>, ChildObjects(<span style="color: #006080;">"Browser"</span>).Page(<span style="color: #006080;">"title:=My App \- Login"</span>)
        .Add <span style="color: #006080;">"Username"</span>, ChildObjects(<span style="color: #006080;">"Page"</span>).WebEdit(<span style="color: #006080;">"html id:=Username"</span>)
        .Add <span style="color: #006080;">"Password"</span>, ChildObjects(<span style="color: #006080;">"Page"</span>).WebEdit(<span style="color: #006080;">"html id:=Password"</span>)
        .Add <span style="color: #006080;">"Submit"</span>, ChildObjects(<span style="color: #006080;">"Page"</span>).WebButton(<span style="color: #006080;">"outertext:=Submit"</span>)
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">With</span>

    <span style="color: #008000;">\'IsContextLoaded is a function that iterates through the Dictionary and checks if the GUI objects "exist"</span>
    Init = IsContextLoaded(ChildObjects)
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'——————————————————————————-</span>

Часть кода, показанного выше, описывает типичный метод Init для GUI Layer класса страницы логина в Web приложении. Тестовые объекты добавляются, как элементы словаря ChildObjects и их свойства определены посредством Дескрипторного программирования (DP). Читатель может легко провести аналогию с Объектным репозиторием (OR). Благодаря этому встроенному методу мы можем быть уверены, что GUI-объекты всегда определяются в одном месте. В конце кода метода вы можете отметить, что он возвращает результат вызова функции IsContextLoaded которая принимает в качестве аргумента словарь, содержащий ChildObjects.

IsContextLoaded определятся в отдельной общей библиотеке, следующим образом:

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> IsContextLoaded(<span style="color: #0000ff;">ByRef</span> htContext)
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'Function: IsContextLoaded</span>
<span style="color: #008000;">\'Checks that the current GUI context is loaded</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Iterates through the htContext (HashTable) items and executes the Exist method with 0 (zero) as parameter.</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' ByRef htContext - As HashTable</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' True/False</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' Meir Bar-Tal, SOLMAR Knowledge Networks Ltd.</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' 11-Nov-2008</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'See Also:</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #0000ff;">Dim</span> ix, items, keys, strDetails, strAdditionalRemarks

    <span style="color: #008000;">\'—————————————————————————</span>
    items = htContext.Items
    keys = htContext.Keys

    <span style="color: #0000ff;">For</span> ix = 0 <span style="color: #0000ff;">To</span> htContext.Count-1
        IsContextLoaded = IsContextLoaded <span style="color: #0000ff;">And</span> items(ix).Exist(0)
        strDetails = strDetails & vbNewLine & <span style="color: #006080;">"Object #"</span> & ix+1 & <span style="color: #006080;">": \'"</span> & keys(ix) & <span style="color: #006080;">"\' was"</span>
        <span style="color: #0000ff;">If</span> IsContextLoaded <span style="color: #0000ff;">Then</span>
            intStatus = micPass
            strDetails = strDetails & <span style="color: #006080;">""</span>
            strAdditionalRemarks = <span style="color: #006080;">""</span>
        <span style="color: #0000ff;">Else</span>
            intStatus = micWarning
            strDetails = strDetails & <span style="color: #006080;">" not"</span>
            strAdditionalRemarks = <span style="color: #006080;">" Please check the object properties."</span>
        <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">If</span>
        strDetails = strDetails & <span style="color: #006080;">" found."</span> & strAdditionalRemarks
    <span style="color: #0000ff;">Next</span>
    <span style="color: #008000;">\'—————————————————————————</span>

    Reporter.ReportEvent intStatus, <span style="color: #006080;">"IsContextLoaded"</span>, strDetails
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'——————————————————————————-</span>

И она возвращает Истину если все объекты, определенные в словаре идентифицированы или Ложь если хотя бы один из объектов не найден. Эта функция универсальная и используется для всех проектов, которые я веду для того чтобы быть уверенным, что QTP не зависнет при попытке выполнения операций с несуществующим GUI-объектом. Еще одно преимущество этого метода в том, что он указывает точно на тот объект, который вы хотите проверить и изменить, делая поддержку значительно более легкой.

 

Инкапсуляция бизнес методов в классы

Следующий шаг, после определения дочерних объектов для тестируемого контента – это определение операций, необходимых для выполнения бизнес-сценариев в рамках данного контента. Это легко реализуется через методы класса. Например, класс Login, описанный выше нуждается в следующих методах для начала работы с ним: SetUsername, SetPassword и Submit. Они показаны ниже:

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> SetUsername()
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'Function: SetUsername</span>
<span style="color: #008000;">\'Set the Username field</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Dependencies:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'——————————————————————————-</span>
    ChildObjects(<span style="color: #006080;">"Username"</span>).<span style="color: #0000ff;">Set</span> GlobalDictionary(<span style="color: #006080;">"Username"</span>)
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'——————————————————————————-</span>

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> SetPassword()
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'Function: SetPassword</span>
<span style="color: #008000;">\'Set the Password field</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Dependencies:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'——————————————————————————-</span>
    ChildObjects(<span style="color: #006080;">"Password"</span>).<span style="color: #0000ff;">Set</span> GlobalDictionary(<span style="color: #006080;">"Password"</span>)
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'——————————————————————————- </span>

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Submit()
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'Function: Submit</span>
<span style="color: #008000;">\'Presses the Submit button</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Dependencies:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'——————————————————————————-</span>
    ChildObjects(<span style="color: #006080;">"Submit"</span>).Click

    <span style="color: #008000;">\'TODO: Verify data submission performed successfully</span>
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'——————————————————————————-</span>

Отметим использование GlobalDictionary для получения требуемых значений для функций (username, password) и использование свойства ChildObjects для получения через тестовый объект ссылки на выполняемый объект.

Следующим шагом будет перемещение на Бизнес Слой (Business Layer), который реализует бизнес сценарий, построенный на базе GUI Слоя (GUI Layer). Например, для того чтобы выполнить логин в систему на основе вышеописанного примера мы реализуем следующую функцию:

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> do_login()
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'Function: do_login</span>
<span style="color: #008000;">\'Implements the business logic of the do_login Action.</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' None</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' Status</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #0000ff;">Dim</span> intStatus, objLogin

    <span style="color: #0000ff;">Set</span> objLogin = CreateLogin()
    <span style="color: #0000ff;">If</span> objLogin.Init() <span style="color: #0000ff;">Then</span>
        objLogin.SetUsername()
        objLogin.SetPassword()
        objLogin.Submit()

        <span style="color: #008000;">\'If login succeeds</span>
        intStatus = micPass
    <span style="color: #0000ff;">Else</span>
        intStatus = micFail
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">If</span>

    do_login = intStatus
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'——————————————————————————-</span>

Отметим использование класса Login описанного выше и его функции Init как меру предосторожности для уверенности, что необходимый контент загружен и не завис, как обсуждалось ранее. Как вы можете видеть, код вышеописанной функции достаточно просто для понимания, и не перегружен ссылками на OR объекты, источники данных как при обычной реализации. Если изменения на GUI затронут объекты в данном контенте – все изменения будут сконцентрированы только в данном пакете, как те что касаются собственно свойств объектов так и механизмов работы с дочерними объектами. Еще одно преимущество данной методики – это стандартизация. Разрабатывая код по данной схеме, мы достигаем высокой степени унификации кода написанного разными разработчиками и таким образом улучшаем управляемость тестового проекта.

Более продвинутая альтернатива последнему примеру – это упаковка таких бизнес-функций, используя Command Wrapper шаблон проектирования, как это описано в моей статье Function Pointers in VB Script (revised). Например:

<span style="color: #008000;">\'VB Script Document</span>
<span style="color: #0000ff;">Option</span> Explicit

<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">Class</span> do_login
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'Class: do_login</span>
<span style="color: #008000;">\'Encapsulates the do_login Action.</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #008000;">\'Methods</span>
    <span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Default</span> <span style="color: #0000ff;">Function</span> Run()
    <span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #008000;">\'Function: Run</span>
    <span style="color: #008000;">\'Implements the business logic of the do_login Action.</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Remarks:</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Arguments:</span>
    <span style="color: #008000;">\' None</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Returns:</span>
    <span style="color: #008000;">\' Status</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Owner:</span>
    <span style="color: #008000;">\' John Doe</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Date:</span>
    <span style="color: #008000;">\' dd-MMM-yyyy</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'——————————————————————————-</span>
        <span style="color: #0000ff;">Dim</span> intStatus

        <span style="color: #0000ff;">Set</span> objLogin = CreateLogin()
        <span style="color: #0000ff;">If</span> objLogin.Init() <span style="color: #0000ff;">Then</span>
            objLogin.SetUsername()
            objLogin.SetPassword()
            objLogin.Submit()

            <span style="color: #008000;">\'If login succeeds</span>
            intStatus = micPass
        <span style="color: #0000ff;">Else</span>
            intStatus = micFail
        <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">If</span>

        Run = intStatus
    <span style="color: #008000;">\'——————————————————————————-</span>
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
    <span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #008000;">\'——————————————————————————-</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>
<span style="color: #008000;">\'——————————————————————————-</span>

Адаптация данной методики делает возможным реализацию продвинутых универсальных контроллеров (generic controller), которые загружают свои сценарии из внешних источников данных, таких как XML файл. Такие управляющие структуры были разработаны мной и моими партнерами в SOLMAR Knowledge Networks как часть нашего универсального фреймворка – Object Oriented comprehensive automation framework – My System.

 

Выводы

В данной статье рассматривался альтернативный подход реализации тестов при автоматизации, основанный на объектно-ориентированной методологии. Я показал, что автоматизацию не следует рассматривать иначе, чем разработку программ. Более того, я постарался отразить, что логически следуя данной мысли, мы приходим к заключению, что проект автоматизации должен рассматриваться как программный проект в идеале, и предложил расширение концепции Объектного Репозитория (OR) (которая показала несоответствие OR методологии ООП) для инкапсуляции интерфейсов GUI объектов: GUI Слой.

В статье также приводился практический пример реализации такого слоя и его вызов, используя Бизнес-Слой, и разъяснены в деталях преимущества такого подхода для достижения максимального эффекта от вложенных инвестиций (ROI) в проекте автоматизации относительно поддерживаемости, читаемости, масштабируемости, расширяемости и тестируемости. Следующие статьи расширят данную тему и покажут читателям, как получить выигрыш от реализации в фреймворке других шаблонов проектирования, помимо изложенных в рамках этой статьи.

, , , , ,

Universal class for data manipulations (Russian)

0
by on April 11, 2009 at 07:18

Sergey Talalaev (SQAdotBY) has contributed this wonderful article on using a generic class for all your Excel data-operations needs. Thanks SQAdotBY, for you hard work, and willingness to share it with the QTP community! This article is copyrighted to Sergey.

An English translation is available here.

Универсальный класс для работы с данными.

Содержание:

 

1. Введение.

2. Excel data manipulation.

2.1. Основные задачи.

2.2. Предварительные шаги.

2.3. Со стороны функциональной библиотеки.

2.4. Со стороны тестового скрипта.

3. Выводы.

 

1. Введение

Влияние мирового кризиса к сожалению сказалось и на моей персоне и мне потребовалось срочно и по возможности глубоко изучить новый для себя фреймворк – Quick Test Professional.

У меня уже был опыт работы с продуктами HP Mercury, но он относился к предыдущей линейке, я имею в виду WinRunner. Естественно я ожидал некоторого сходства в процессах скриптостроения и организации самого фреймворка. Поэтому по свежим следам постараюсь изложить замеченные мною интересные моменты и привести свои примеры реализации некоторых функций.

2. Работа с Excel-данными

Еще работая с WinRunner, я убедился, что встроенная реализация Excel хранилища не настолько гибка, как мне бы хотелось. Поэтому, как всегда, я приготовился к миграции своих процедур для работы с Excel. И был немного удивлен отсутствию встроенных средств работы с БД. Но это задача вполне по силам, когда за плечами вся мощь VB.

2.1. Основные задачи

Итак, для начала определимся, зачем нам это нужно.

Я всегда выступал за то, чтобы подготовка тестовых данные для автоматических тестов была максимально упрощена с одной стороны, и, по возможности, исключала возможность ошибок при вводе с другой.

Эти трудносовместимые вещи отлично реализуются посредством встроенной в Excel валидации входных данных и также прекрасно уничтожаются попыткой редактирования Excel таблицы напрямую из QTP.

Более того уничтожается также любое кастомное форматирование, без которого нормально читаемая таблица превращается в клетчатый текст.

Было до вмешательства из QTP:

clip_image002

Стало после редактирования из QTP:

clip_image004

Чтобы избежать подобных казусов я давно применяю прямую вычитку из Excel файлов в массивы, используя для этого стандартные ODBC источники. Данная техника успешно прижилась уже на следующих тестовых фреймворках: Rational Robot, IBM Rational Functional Tester, WinRunner и надеюсь QTP

Помимо решения вышеперечисленных проблем мы избегаем также серьезного, на мой взгляд, ограничения по использованию одной таблицы для одного Excel sheet.

2.2. Предварительные шаги

В Excel документах имеется функциональность которая позволяет выделять значимые для пользователя подмножества ячееек в специальные структуры. Такие структуры называемые “именованными диапазонами” обеспечивают возможность обращаться к ним к ним через логические имена.

Кроме того (что гораздо более значимо для нас) такие диапазоны становятся видны как стандартные ODBC таблицы из внешних приложений.

Итак, для оформления требуемой совокупности ячеек в качестве “именованного диапазона” необходимо выполнить следющую последовательность действий:

- выделить все ячейки, планируемые для оформления в именованный диапазон

- создать именованный диапазон через “Define name” диалог (Formulas > Define Name)

clip_image006

или напрямую введя имя диапазона в Navigation Bar.

clip_image008

Для проверки корректности вновь созданного именованного диапазона – выделите все ячейки диапазона и проверьте значение в Navigation Bar. Он должен содержать логическое имя вместо A1 нотации.

Важно отметить одно требование обязательно при использовании именованных диапазонов в качестве источника данных:

- первая строка нашего диапазона должна содержать имена столбцов, а не данные

2.3. Со стороны функциональной библиотеки

После всех подготовительных шагов осталось совсем немного поработать руками, а точнее пописать код.

Раз уж QTP 9.5 предоставил нам замечательную возможность работать c “почти” объектами – грех было бы ей не воспользоваться. Поэтому весь наш функционал мы гордо завернем в класс с благозвучным названием TestData.

Стоит напомнить, что QTP не видит напрямую классы, объявленные во внешних библиотеках, поэтому для каждого класса должна присутствовать функция создания экземпляра класса, в данном случае – CreateTestData

Кроме того мы должны иметь возможность инициировать наш класс не только через загрузку из Excel источника но и напрямую из кода. Именно для этих целей появились два метода: SetData и GetData

 

 

<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> CreateTestData ()
    <span style="color: #0000ff;">Set</span> CreateTestData = <span style="color: #0000ff;">new</span> TestData
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

<span style="color: #0000ff;">Class</span> TestData

    <span style="color: #0000ff;">Private</span> mTestTable()

    <span style="color: #0000ff;">Private</span> <span style="color: #0000ff;">Sub</span> Class_Initialize()
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

    <span style="color: #0000ff;">Private</span> <span style="color: #0000ff;">Sub</span> Class_Terminate()
        <span style="color: #0000ff;">Erase</span> mTestTable
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

    <span style="color: #008000;">\' @Documentation Initiates TestData object with external data</span>
    <span style="color: #008000;">\'-----------------------------------------------------------</span>
    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Sub</span> SetData (DataArr())
        <span style="color: #0000ff;">Dim</span> i,j
        <span style="color: #0000ff;">ReDim</span> mTestTable(UBound(DataArr, 1), UBound(DataArr, 2))

        <span style="color: #0000ff;">For</span> i=0 <span style="color: #0000ff;">to</span> UBound(DataArr, 1)
            <span style="color: #0000ff;">For</span> j=0 <span style="color: #0000ff;">to</span> UBound(DataArr, 2)
                mTestTable(i,j) = DataArr(i,j)
            <span style="color: #0000ff;">Next</span>
        <span style="color: #0000ff;">Next</span>
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

    <span style="color: #008000;">\' @Documentation Extracts data from TestData object</span>
    <span style="color: #008000;">\'---------------------------------------------------</span>
    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Sub</span> GetData(DataArr())
        <span style="color: #0000ff;">Dim</span> i,j

        <span style="color: #0000ff;">ReDim</span> DataArr(UBound(mTestTable, 1), UBound(mTestTable, 2))

        <span style="color: #0000ff;">For</span> i=0 <span style="color: #0000ff;">to</span> UBound(mTestTable, 1)
            <span style="color: #0000ff;">For</span> j=0 <span style="color: #0000ff;">to</span> UBound(mTestTable, 2)
                DataArr(i,j) = mTestTable(i,j)
            <span style="color: #0000ff;">Next</span>
        <span style="color: #0000ff;">Next</span>
    <span style="color: #0000ff;">End</span> Sub
          <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

Итак, мы вплотную подобрались к центральной части нашего функционала – вычитке данных из Excel источника.

За данную часть функционала отвечают два взаимосвязанных метода: GetArrayFromStore и LoadFromStore

Первый позволяет нам выгрузить вычитанные данные во внешний массив, минуя наш класс, а второй наоборот – инициирует наш класс вычитанными данными.

<span style="color: #008000;">\' @Documentation Extracts test data from the Excel store</span>
<span style="color: #008000;">\'------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Sub</span> GetArrayFromStore(Arr(), TableName, StoreName)

    <span style="color: #0000ff;">Dim</span> Connection
    <span style="color: #0000ff;">Dim</span> i, j, fieldcount, rowsfetched
    <span style="color: #0000ff;">Dim</span> ArrTemp()
    <span style="color: #0000ff;">Dim</span> retcode
    <span style="color: #0000ff;">Dim</span> RepPath, BufStr

    <span style="color: #0000ff;">Set</span> Connection = CreateObject(<span style="color: #006080;">"ADODB.Connection"</span>)

    Connection.ConnectionString = <span style="color: #006080;">"DBQ="</span> + StoreName + _
        <span style="color: #006080;">";Driver={Microsoft Excel Driver (*.xls)}"</span> + _
        <span style="color: #006080;">";DriverId=790;FIL=excel 8.0;MaxBufferSize=2048"</span> + _
        <span style="color: #006080;">";MaxScanRows=8;PageTimeout=5;ReadOnly=1"</span> + _
        <span style="color: #006080;">";SafeTransactions=0;Threads=3;UserCommitSync=Yes"</span>

    Connection.Open
    <span style="color: #0000ff;">Set</span> ConnRs = CreateObject(<span style="color: #006080;">"ADODB.Recordset"</span>)

    ConnRs.CursorType = 3
    <span style="color: #0000ff;">Call</span> ConnRs.Open(<span style="color: #006080;">"select * from "</span> + TableName, Connection)

    fieldcount = ConnRs.Fields.Count
    <span style="color: #0000ff;">Redim</span> ArrTemp(fieldcount - 1, 0)

    <span style="color: #008000;">\'column names adding</span>
    <span style="color: #0000ff;">for</span> j=0 <span style="color: #0000ff;">to</span> fieldcount - 1
        ArrTemp(j,0) = ConnRs.Fields(j).Name
    <span style="color: #0000ff;">next</span>

    rowsfetched = 0
    ConnRs.MoveFirst()

    <span style="color: #0000ff;">While</span> <span style="color: #0000ff;">not</span> ConnRs.Eof
        rowsfetched = rowsfetched + 1
        <span style="color: #0000ff;">Redim</span> <span style="color: #0000ff;">Preserve</span> ArrTemp(fieldcount - 1, rowsfetched)

        <span style="color: #0000ff;">for</span> j=0 <span style="color: #0000ff;">to</span> fieldcount - 1
            ArrTemp(j, rowsfetched + i) = ConnRs(j).value
        <span style="color: #0000ff;">next</span>
        ConnRs.MoveNext()
    <span style="color: #0000ff;">Wend</span>

    Connection.Close

    <span style="color: #008000;">\'matrix transposition</span>
    <span style="color: #0000ff;">Redim</span> Arr(UBound(ArrTemp,2), UBound(ArrTemp,1))

    <span style="color: #0000ff;">for</span> i=LBound(ArrTemp,1) <span style="color: #0000ff;">to</span> UBound(ArrTemp,1)
        <span style="color: #0000ff;">for</span> j=LBound(ArrTemp,2) <span style="color: #0000ff;">to</span> UBound(ArrTemp,2)
            Arr(j,i) = ArrTemp(i,j)
        <span style="color: #0000ff;">next</span>
    <span style="color: #0000ff;">next</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

<span style="color: #008000;">\' @Documentation Loads test data from Excel store to the TestData object</span>
<span style="color: #008000;">\'-----------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Sub</span> LoadFromStore (TableName, StoreName)
    <span style="color: #0000ff;">call</span> GetArrayFromStore(mTestTable, TableName, StoreName)
<span style="color: #0000ff;">End</span> Sub

Как вы можете заметить, ничего сверхъественного в реализации данного функционала нет. Более того очевидно, что с небольшими модификациями такую же процедуру можно применять для вычитки данных из любого ODBC источника.

Нам осталось добавить несколько сервисных методов, чтобы наш класс стал действительно удобным в использовании: GetCellByIndex, GetCellByName, ColumnCount, RowCount

 

<span style="color: #008000;">\' @Documentation Gets cell value by row-column indexes</span>
<span style="color: #008000;">\'------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> GetCellByIndex (RowIndex, ColumnIndex)
    GetCellByIndex = mTestTable(RowIndex+1, ColumnIndex)
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

<span style="color: #008000;">\' @Documentation Gets cell value by column name and row index</span>
<span style="color: #008000;">\'-------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> GetCellByName (ColumnName, RowIndex)
    <span style="color: #0000ff;">Dim</span> j

    <span style="color: #0000ff;">for</span> j = 0 <span style="color: #0000ff;">to</span> UBound(mTestTable,2)
        <span style="color: #0000ff;">If</span> (ColumnName = mTestTable(0,j)) <span style="color: #0000ff;">Then</span>
            <span style="color: #0000ff;">Exit</span> <span style="color: #0000ff;">For</span>
        <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">if</span>
    <span style="color: #0000ff;">next</span>
    GetCellByName = mTestTable(RowIndex+1, j)
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

<span style="color: #008000;">\' @Documentation Returns number of columns into the test data table</span>
<span style="color: #008000;">\'------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> ColumnCount()
    ColumnCount = UBound(mTestTable, 2)
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

<span style="color: #008000;">\' @Documentation Returns number of rows into the test data table</span>
<span style="color: #008000;">\'---------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> RowCount()
    RowCount = UBound(mTestTable, 1) - 1
<span style="color: #0000ff;">End</span> Function

2.4. Со стороны тестового скрипта

Как же будет выглядеть минимально необходимый набор операций для инициализации нашего класса и вычитки данных на стороне скрипта.

<span style="color: #0000ff;">Dim</span> TestArr() ‘Dynamic array
<span style="color: #0000ff;">Dim</span> TestD

<span style="color: #0000ff;">Set</span> TestD = CreateTestData()
Сall TestD.LoadFromStore(<span style="color: #006080;">"NamedRange"</span>, <span style="color: #006080;">"C:\ExcelStore.xls"</span>)
Сall TestD.GetArrayFromStore(TestArr, <span style="color: #006080;">"NamedRange"</span>, <span style="color: #006080;">"C:\ExcelStore.xls"</span>)
‘<span style="color: #0000ff;">Call</span> TestD.GetArrayFromURL(TestArr, <span style="color: #006080;">"OrgChart@"</span>+ <span style="color: #006080;">"C:\ExcelStore.xls"</span>)
<span style="color: #0000ff;">Call</span> TestD.SetData(TestArr)

<span style="color: #0000ff;">Erase</span> TestArr

<span style="color: #0000ff;">Call</span> TestD.GetData(TestArr)

Print TestArr(1,1)
Print TestD.GetCellByIndex(2,2)
Print TestD.GetCellByName(<span style="color: #006080;">"Name"</span>, 2)
Print TestD.RowCount
Print TestD.ColumnCount

Обращаю ваше внимание, что массив (если вы решите выгружать данные из TestData объекта) должен быть динамическим, а не статическим.

Для более глубокого понимания функционала предлагаю пройтись по нему в режиме отладки и внимательно проконтролировать весь процесс.

Вы также наверняка заметили одну закомментированную функцию, я имею в виду GetArrayFromURL.

Я использую данную функцию для удобной реализации вложенных данных. В этом случае на уровне Excel каждая строка должна содержать дополнительную колонку, в которой хранится URL связанного массива данных.

clip_image010

Предлагаю вам реализовать данную функциональность самостоятельно.

3. Выводы

Возможно, кого-то, во время прочтения статьи, не покидало ощущение бесполезности реализации дополнительной функциональности имея стандартные средства работы с Eхcel. Частично я уже объяснял причины необходимости этого выше (Основные задачи). Но кроме этого хотелось бы еще добавить в копилку знаний несколько замечаний.

В силу особенностей моей специализации (функциональное автоматизированное тестирование) мне не удается работать с каким-то одним фреймворком. Тем не менее, всегда очень хочется использовать предыдущие наработки и обустроить свою работу максимально комфортно. Как оказалось это совсем не сложно и позволяет в итоге работать в привычных комфортных условиях, максимально используя уже проверенные схемы.

, ,

Universal class for data manipulations

0
by on April 11, 2009 at 07:05

Sergey Talalaev (SQAdotBY) has contributed this wonderful article on using a generic class for all your Excel data-operations needs. Thanks SQAdotBY, for you hard work, and willingness to share it with the QTP community! This article is copyrighted to Sergey.

A Russian translation is available here.

Content:

1.          Introduction.

2.          Excel data manipulation.

2.1.       Main goals

2.2.       Preliminary steps

2.3.       From functional library side.

2.4.       From the test script side.

3.          Conclusions.

1.   Introduction

I had previous experience with HP Mercury products, but only related to the previous product line – WinRunner. Naturally, I expected some kind of similarity in the script development and the whole framework organization. Therefore I’d like to note all detected interesting moments and provide my examples of some suitable functionality realization.

2.   Excel data manipulation

Working with WinRunner I got evidence that the standard realization of Excel data storage is not as flexible as I want. Therefore, starting my work with QTP, I’ve started to analyze the possibilities for migrating my Excel related library to QTP. And I was a little embarrassed about the lack of standard tools for direct DB manipulations.

 

2.1.    Main goals

 

Firstly let’s decide why it’s really useful for us.

I’ve always adhere the opinion that test data preparation process should be as easy as possible on one hand, and give ability to avoid errors on the other hand.

These hardly compatible goals can be perfectly realized by Excel data validation functionality, but it is perfectly destroyed after modification from QTP. Moreover, also destroyed are any custom formatting and without it well formatted Excel table changed into the chequered text.

 

Before modification by QTP:

clip_image002

 

After modification by QTP:

clip_image004

 

To avoid the problems I always use direct loading from Excel into arrays via standard ODBC functionality. This technique was successfully implemented for next test frameworks: Rational Robot, IBM Rational Functional Tester, WinRunner and I hope QTP. In addition to resolving all problems above we avoid serious restriction about using only one data table for one Excel sheet.

2.2.    Preliminary steps

 

There is a not so famous functionality in MS Excel, which allows separating important for user subset of cells into special structures. These structures are called “named ranges” and provide possibility of referencing to it via logical names. More over (and more significantly for us) these ranges become visible as a standard ODBC tables from external applications out of Excel.

So, for declaration chosen cell subset as a named range we need to perform next steps:

-       select all cells planned to be marked as named range

-       create named range via “Define name” dialog (Formulas > Define  Name)

 

clip_image006

 

or directly, entering name of the range into Navigation Bar.

 

clip_image008

 

To check correctness just created range – select all range cells and check value into

 Navigation Bar. It should be equal to range name instead of standard A1 notation.

 

It’s important to note one mandatory requirement if you are going to use named range functionality:

-       first row of your range should contains columns names, not the data

 

2.3.    From functional library side

 

After all preliminary steps, it’s only remains a little part of handwork, I mean coding.

QTP 9.5 supports objects and it’s reasonable to use this suitable functionality. Therefore all our code we embed into the class TestData.

 

I’d like to remind that QTP doesn’t see directly classes declared into external library files, especially because of this we need to have function that create class instance, in our case it’s CreateTestData

Besides that it’s a good idea to have possibility for initialization our class not only from Excel store but from arrays directly from the code. Especially for that additional two methods were implemented: SetData and GetData

.

 

<span style="color: #0000ff">Public</span> <span style="color: #0000ff">Function</span> CreateTestData ()
    <span style="color: #0000ff">Set</span> CreateTestData = <span style="color: #0000ff">new</span> TestData
<span style="color: #0000ff">End</span> <span style="color: #0000ff">Function</span>
 
<span style="color: #0000ff">Class</span> TestData
          <span style="color: #0000ff">Private</span> mTestTable()
 
          <span style="color: #0000ff">Private</span> <span style="color: #0000ff">Sub</span> Class_Initialize()
          <span style="color: #0000ff">End</span> <span style="color: #0000ff">Sub</span>
 
          <span style="color: #0000ff">Private</span> <span style="color: #0000ff">Sub</span> Class_Terminate()
             <span style="color: #0000ff">Erase</span> mTestTable
          <span style="color: #0000ff">End</span> <span style="color: #0000ff">Sub</span>
 
          <span style="color: #008000">\' @Documentation Initiates TestData object with external data</span>
          <span style="color: #008000">\'-----------------------------------------------------------</span>
          <span style="color: #0000ff">Public</span> <span style="color: #0000ff">Sub</span> SetData (DataArr())
                   <span style="color: #0000ff">Dim</span> i,j
 
                   <span style="color: #0000ff">ReDim</span> mTestTable(UBound(DataArr, 1), UBound(DataArr, 2))
                   <span style="color: #0000ff">For</span> i=0 <span style="color: #0000ff">to</span> UBound(DataArr, 1)
                             <span style="color: #0000ff">For</span> j=0 <span style="color: #0000ff">to</span> UBound(DataArr, 2)
                                      mTestTable(i,j) = DataArr(i,j)
                             <span style="color: #0000ff">Next</span>
                   <span style="color: #0000ff">Next</span>
          <span style="color: #0000ff">End</span> <span style="color: #0000ff">Sub</span>
 
 
          <span style="color: #008000">\' @Documentation Extracts data from TestData object</span>
          <span style="color: #008000">\'---------------------------------------------------</span>
          <span style="color: #0000ff">Public</span> <span style="color: #0000ff">Sub</span> GetData(DataArr())
                   <span style="color: #0000ff">Dim</span> i,j
                   <span style="color: #0000ff">ReDim</span> DataArr(UBound(mTestTable, 1), UBound(mTestTable, 2))
                   <span style="color: #0000ff">For</span> i=0 <span style="color: #0000ff">to</span> UBound(mTestTable, 1)
                             <span style="color: #0000ff">For</span> j=0 <span style="color: #0000ff">to</span> UBound(mTestTable, 2)
                                      DataArr(i,j) = mTestTable(i,j)
                             <span style="color: #0000ff">Next</span>
                   <span style="color: #0000ff">Next</span>
          <span style="color: #0000ff">End</span> <span style="color: #0000ff">Sub</span>
          <span style="color: #0000ff">End</span> <span style="color: #0000ff">Class</span>

 

So we are now as much closer as possible to implementation the core functionality module – data loading from Excel. There are two closely interconnected methods responsible for that: GetArrayFromStore and LoadFromStore. First one allows us to extract loading data into external array not involving internal class store, second one vice versa – initiates our class by loaded data.

 

<span style="color: #008000">\' @Documentation Extracts test data from the Excel store</span>
<span style="color: #008000">\'------------------------------------------------------</span>
<span style="color: #0000ff">Public</span> <span style="color: #0000ff">Sub</span> GetArrayFromStore(Arr(), TableName, StoreName)
         <span style="color: #0000ff">Dim</span> Connection
         <span style="color: #0000ff">Dim</span> i, j, fieldcount, rowsfetched
         <span style="color: #0000ff">Dim</span> ArrTemp()
         <span style="color: #0000ff">Dim</span> retcode
         <span style="color: #0000ff">Dim</span> RepPath, BufStr
 
         <span style="color: #0000ff">Set</span> Connection = CreateObject(<span style="color: #006080">"ADODB.Connection"</span>)
 
         Connection.ConnectionString = <span style="color: #006080">"DBQ="</span> + StoreName + _
                                      <span style="color: #006080">";Driver={Microsoft Excel Driver (*.xls)}"</span> + _
                                      <span style="color: #006080">";DriverId=790;FIL=excel 8.0;MaxBufferSize=2048"</span> + _
                                      <span style="color: #006080">";MaxScanRows=8;PageTimeout=5;ReadOnly=1"</span> + _
                            <span style="color: #006080">";SafeTransactions=0;Threads=3;UserCommitSync=Yes"</span>
         Connection.Open
         <span style="color: #0000ff">Set</span> ConnRs = CreateObject(<span style="color: #006080">"ADODB.Recordset"</span>)
 
         ConnRs.CursorType = 3
         <span style="color: #0000ff">Call</span> ConnRs.Open(<span style="color: #006080">"select * from "</span> + TableName, Connection)
         fieldcount = ConnRs.Fields.Count
         <span style="color: #0000ff">Redim</span> ArrTemp(fieldcount - 1, 0)
 
         <span style="color: #008000">\'column names adding</span>
         <span style="color: #0000ff">for</span> j=0 <span style="color: #0000ff">to</span> fieldcount - 1
                   ArrTemp(j,0) = ConnRs.Fields(j).Name
         <span style="color: #0000ff">next</span>
 
         rowsfetched = 0
         ConnRs.MoveFirst()
 
         <span style="color: #0000ff">While</span> <span style="color: #0000ff">not</span> ConnRs.Eof
                   rowsfetched = rowsfetched + 1
                   <span style="color: #0000ff">Redim</span> <span style="color: #0000ff">Preserve</span> ArrTemp(fieldcount - 1, rowsfetched)
 
                   <span style="color: #0000ff">for</span> j=0 <span style="color: #0000ff">to</span> fieldcount - 1
                            ArrTemp(j, rowsfetched + i) = ConnRs(j).value
                   <span style="color: #0000ff">next</span>             
                   ConnRs.MoveNext()
         <span style="color: #0000ff">Wend</span>
 
         Connection.Close
 
         <span style="color: #008000">\'matrix transposition</span>
         <span style="color: #0000ff">Redim</span> Arr(UBound(ArrTemp,2), UBound(ArrTemp,1))
         <span style="color: #0000ff">for</span> i=LBound(ArrTemp,1) <span style="color: #0000ff">to</span> UBound(ArrTemp,1)
                   <span style="color: #0000ff">for</span> j=LBound(ArrTemp,2) <span style="color: #0000ff">to</span> UBound(ArrTemp,2)
                            Arr(j,i) = ArrTemp(i,j)
                   <span style="color: #0000ff">next</span>
         <span style="color: #0000ff">next</span>
<span style="color: #0000ff">End</span> <span style="color: #0000ff">Sub</span>
 
<span style="color: #008000">\' @Documentation Loads test data from Excel store to the TestData object</span>
<span style="color: #008000">\'-----------------------------------------------------------------------</span>
<span style="color: #0000ff">Public</span> <span style="color: #0000ff">Sub</span> LoadFromStore (TableName, StoreName)
         <span style="color: #0000ff">call</span> GetArrayFromStore(mTestTable, TableName, StoreName)
<span style="color: #0000ff">End</span> Sub

As you may see during our functional implementation we use only ordinary functionality and nothing from “braincrackers” equipment. Moreover it’s obviously that after small modifications we can use our class for direct loading from any other ODBC source.

We need to add several service methods to make our class really suitable: GetCellByIndex, GetCellByName, ColumnCount, RowCount

<span style="color: #008000">\' @Documentation Gets cell value by row-column indexes</span>
<span style="color: #008000">\'------------------------------------------------------</span>
<span style="color: #0000ff">Public</span> <span style="color: #0000ff">Function</span> GetCellByIndex (RowIndex, ColumnIndex)
         GetCellByIndex = mTestTable(RowIndex+1, ColumnIndex)
<span style="color: #0000ff">End</span> <span style="color: #0000ff">Function</span>
 
<span style="color: #008000">\' @Documentation Gets cell value by column name and row index</span>
<span style="color: #008000">\'-------------------------------------------------------------</span>
<span style="color: #0000ff">Public</span> <span style="color: #0000ff">Function</span> GetCellByName (ColumnName, RowIndex)
         <span style="color: #0000ff">Dim</span> j
 
         <span style="color: #0000ff">for</span> j = 0 <span style="color: #0000ff">to</span> UBound(mTestTable,2)
                   <span style="color: #0000ff">If</span> (ColumnName = mTestTable(0,j)) <span style="color: #0000ff">Then</span> 
                            <span style="color: #0000ff">Exit</span> <span style="color: #0000ff">For</span> 
                   <span style="color: #0000ff">End</span> <span style="color: #0000ff">if</span>
         <span style="color: #0000ff">next</span>
 
         GetCellByName = mTestTable(RowIndex+1, j)
<span style="color: #0000ff">End</span> <span style="color: #0000ff">Function</span>
 
<span style="color: #008000">\' @Documentation Returns number of columns into the test data table</span>
<span style="color: #008000">\'------------------------------------------------------------------</span>
<span style="color: #0000ff">Public</span> <span style="color: #0000ff">Function</span> ColumnCount()
         ColumnCount = UBound(mTestTable, 2)
<span style="color: #0000ff">End</span> <span style="color: #0000ff">Function</span>
 
<span style="color: #008000">\' @Documentation Returns number of rows into the test data table</span>
<span style="color: #008000">\'---------------------------------------------------------------</span>
<span style="color: #0000ff">Public</span> <span style="color: #0000ff">Function</span> RowCount()
         RowCount = UBound(mTestTable, 1) - 1
<span style="color: #0000ff">End</span> Function

 

2.4.    From the test script side

 

What are the minimal required steps needed for using implemented functionality from test script side. As you may see from example below you just need to declare our object and initiate it by CreateTestData function.

 
<span style="color: #0000ff">Dim</span> TestArr()   ‘Dynamic array
<span style="color: #0000ff">Dim</span> TestD
 
<span style="color: #0000ff">Set</span> TestD = CreateTestData()
 
Сall TestD.LoadFromStore(<span style="color: #006080">"NamedRange"</span>, <span style="color: #006080">"C:\ExcelStore.xls"</span>)
Сall TestD.GetArrayFromStore(TestArr, <span style="color: #006080">"NamedRange"</span>, <span style="color: #006080">"C:\ExcelStore.xls"</span>)
‘<span style="color: #0000ff">Call</span> TestD.GetArrayFromURL(TestArr, <span style="color: #006080">"OrgChart@"</span>+ <span style="color: #006080">"C:\ExcelStore.xls"</span>)
 
<span style="color: #0000ff">Call</span> TestD.SetData(TestArr)
<span style="color: #0000ff">Erase</span> TestArr
 
<span style="color: #0000ff">Call</span> TestD.GetData(TestArr)
 
Print TestArr(1,1)
Print TestD.GetCellByIndex(2,2)
Print TestD.GetCellByName(<span style="color: #006080">"Name"</span>, 2)
Print TestD.RowCount
Print TestD.ColumnCount

I’d like to pay your attention to the following fact – array (if you decide to use extract data from TestData object) should be declared as dynamic, not static.

For deeper functional understanding I suggest to perform the code under debug mode and carefully check all the process.

 

You probably detected one commented function, I mean GetArrayFromURL.

I’ve use this function for suitable implementation nested data. In that case into your Excel tables additional column should be added, containing URL for nested data table (see picture below).

clip_image010

 

I suggest realizing this functionality yourself.

 

3.   Conclusions

 

Probably, during reading the article, you had ideas about uselessness additional functionality realization for Excel having standard ones. Partially I’ve described the main reasons of this activity above (see Main goals). But in addition to all said above I’d like to add into the moneybox several valuable notes.

Because of my specialty (functional test automation) and differences between the projects to be tested I have no ability to work constantly with one test framework. But in spite of this I always try to maximize reusing more successful techniques and practices. As you can see it’s not so difficult for some cases and gives ability to work in comfortable, habitual environment.

AdvancedQTP would like to thanks Sergey again, for all his hard work!

, ,

Implementing a GUI Layer with Classes

1
by on December 20, 2008 at 19:46

A Russian version of this article is available here

 

Abstract

This article describes a powerful technique that exploits Object Oriented Design Patterns, QTP descriptive programming (DP) and the Dictionary object to pack together GUI objects with their corresponding business oriented functions. The article includes a valuable bonus: a highly effective tip that enables to exit a test smoothly, preventing QTP from getting stuck when it fails to identify GUI objects during runtime.

 

Introduction

A central issue in automation development is how to reduce the effort spent on maintenance. Questions such as: “Should we use an Object Repository (OR) or Descriptive Programming (DP)? if an OR is chosen, then should we use a shared OR or a local OR for each Action? If DP is selected, then what is the optimal way to implement it?” are quite ubiquitous and the answers given can vary depending on the particular characteristics of the project, but many times also on the individual personalities involved.

In this article I will analyze the concept of Test Objects in the context of Object Oriented principles. I shall attempt to show that QTP’s Object Repository does not fit well with such principles and what is the impact of this on automation development viz-a-viz cost-effectiveness and maintainability. Subsequently, I will describe an expansion of the OR concept that is compatible with OO principles – the GUI Layer. This concept has been adopted by SOLMAR’s automation experts based on the observation that it is possible to maximize code reusability (and thus boosting maintainability) by undertaking an OO approach that breaks an automation project into several abstraction levels.

 

Test Objects and Run-Time Objects

A test object encapsulates a reference to a run-time object – the real world object that is instantiated in the application under test (AUT). A test object stores the description of the referenced object, which is a set of attributes and values that serve as criteria to locate the right object during the test run session. This description is pretty much like the one we keep in mind to identify our blind date partner at the meeting time (“I’m brunette, slim and tall, and will be wearing a red dress and shoes”, you’ve been told on the phone).

Similarly, QTP uses such a description to query the OS if there’s an object currently loaded to the machine’s RAM. If the answer is positive, the OS returns the address of the object (a reference or pointer) and hence QTP gains access to the object’s public methods and properties, since the private methods and properties are hidden from view by definition (so QTP never makes progress to the private domain, as you would with the brunette, if lucky!).

Though this description may seem overly simplified, basically it is this process that enables us to interact with the GUI (and other) objects using QTP or other automation tools (Test Partner, Test Complete, Rational Robot, AutoIt, etc.). A more detailed account of the distinction between Test Objects and Run-Time Objects is available in Yaron Assa’s article “Differences and Connections between Runtime Objects and Test Objects” (2009).

 

Test Objects and the Object Repository

The above account of test objects makes it seem only natural to think of them as data entities. What do I mean by this? Based on a description composed of a set of properties and values, QTP is able to identify a GUI object among all the objects currently loaded to the machine’s RAM. This is similar to the retrieval of a particular record from a database table based on a SQL WHERE clause. The difference is that in this case, it is the OS (Windows), not the DB, the one that returns the record. Moreover, this record contains a single field that stores a reference to the actual runtime object. Following this line of thought, it seems only natural to have these entities – Test Objects – stored in a database, which essentially is exactly what the Object Repository is (a side note, if you noticed the bdb suffixed files in QTP tests, then note that bdb actually stands for Berkeley Data Base – an Oracle product).

What, then, seems to be the problem with the OR approach? Tests Objects are really data entities, but at the end of the day we need to perform actions on Runtime objects, insomuch that the above account leaves us with an incomplete picture. I will explain below why.

It is true that the OR, if used properly, reflects a best practice according to which we ought to store the description of each GUI (Test) Object only once to reduce maintenance costs. However, in order to achieve this a great effort must be invested also into managing the automation project properly, because ad-hoc requests and resource limitations can endanger the integrity of such a precious resource. For instance, suppose that two automation developers work simultaneously with a shared repository, but one is maintaining the scripts for the last version and the other is developing new scripts for the newest one. Given that GUI changes are made quite regularly from one AUT version to the next, it will not be too long until the OR becomes cluttered with new, redundant, test objects required by the developer working on the newest version. And this is a relatively good result. The worst case would be one where each developer would change in turn the descriptions of existing objects to match their requirements, damaging irrevocably the ROI on the automation project as a whole. Of course, there are solutions to such situations, such as keeping separate versions of the OR that match each AUT version and using configuration management software in the automation project. But, as aforesaid, this requires good project management, which is not always available.

Moreover, keeping versions of the OR leaves one issue still open. Because automated tests are, in general, regressive by nature, then maintenance would certainly pertain not only to the OR but also that the code that implements the actual GUI manipulations and verifications, as well as the input data and expected results data. If the common situation would have been that only the object descriptions changed from one AUT version to the next, then using an OR would be an excellent solution. However, more often than not, GUI changes reflect modifications in the way the AUT actually functions, which are many times also accompanied by even deeper changes – in the database structure, for example. So, changes in the AUT would require matching changes in both the OR and the scripts that refer to its test objects, as well as in the data sources that feed these scripts. Taking into account the fact that scripts are most of the time maintained, not developed, the aforementioned line of analysis lead me to the conclusion that something in the OR concept is faulty regarding the manageability of a large-scale automation project.

 

The Object Oriented View

The Object Oriented software paradigm is based on the basic concepts of encapsulation, inheritance and polymorphism. Encapsulation basically means to pack together functionality and data structures that belong together, and the package through which we achieve this is called a class. In fact, classes mostly are representations of data entities (like Customer, Product, etc.). The difference between a fully featured class and a mere data structure (as in the C programming language) is that the class also wraps the functions available for use with the class data fields. These are commonly referred to as the class methods. For example, a typical class for a Customer would pack the data fields that define the customer as a data entity (customerId, firstName, lastName, phoneNo, etc.) together with functions such as setCustomerId, getCustomerId, which assign and retrieve the value of the respective field, as well as others like getCustomerAge and getCustomerBalance which would perform a calculation based on the class fields current values and then return the result.

Inheritance is deeply related to one of the major goals of the OO approach – reusability. In OO programming languages like C++ and Java, reusability comes into play by means of the ability to device a new class that “inherits” the fields and methods of a previously defined class (also called a base class) and expands on these according to the specific requirements that raised the need for the new class. Polymorphism is yet another powerful concept that enables one to design more than one version of a function (with the same name but different number or types of arguments) to be able to handle transparently different situations within the application context. For instance, we might need to handle numbers of different types (float, int) and so instead of having a single function with conditional statements that check for the value type that was passed, we’ll have several functions with the same name but different signatures (different type of arguments). Hence in our code that uses these functions we would not need to use casting; we would use the same interface in both cases, with the correct process ultimately invoked by the runtime environment. However in QTP these two last concepts of inheritance and polymorphism cannot be implemented based on the VB Script language which provides very rudimentary support for working with classes. Nevertheless, we shall see later that this is not a reason to refrain from using classes in testing automation.

The public and private notions mentioned above are of great importance with regard to encapsulation and inheritance. They enable the code developer to determine which fields (also called properties or attributes of the class) and methods will be actually accessible to the world outside the class (public), and which will remain for internal (private) use of the class members (i.e., the class fields and methods). The OO methodology is not new and has been widely put into practice in the software design and development industry. It is recognized as one approach that makes the resulting code more reusable, maintainable, readable, scalable, extensible and, yes, also more testable (as huge pieces of functionality are broken down into smaller, self-contained capsules or packages). Of course, making good use of the methodology requires deep analytical skills that enable the code designer to infer correctly from the requirements of the software under construction which are the appropriate entities involved and how they are interrelated.

How this relates to the way we approach, or should approach, the testing automation challenge? Let us delve into it below.

 

Testing Automation and Software Development

One thing I have witnessed throughout my career is that QA professionals of all levels treat testing automation as just another testing activity. Not surprisingly, it is not rare to find automation professionals that think likewise since many times the role of automation. Sometimes this is even reflected by the job title given to the automation staff: “automatic tester” is one of the absurd titles I have encountered several years ago. This reflects a misunderstanding of the role of the Automation Developer and, as I shall explain in what follows, this belief is a true misconception of the essence of testing automation.

The task of automating tests should not be treated differently than any other content specific automation task. Generally speaking, computer programs that perform operations instead of humans implement automation. So basically any block of code is a kind of automation device, or robot. Scripts that carry out operations on GUI objects – such as QTP tests – are not different than any other piece of software. Put it in other words, a testing automation project is, indeed, a specific kind of software development project. As with any software product, an automation project also has its own SRS (Software Requirements Specifications) document: the STD (Software Test Design) “document” (or tests design in a quality management tool such as HP’s Quality Center or Orcanos’ QPack), which should guide the automation developer in the implementation of the required code.

If so, then an automation project should be treated and managed as any other development project and, in fact, even more so. This is because the automation developer faces challenges that are usually not felt by the developers of the AUT. Some of the main challenges are the following:

  1. First, the automation developer must have an overall view of the AUT, as the automation scripts may cover a large part of the AUT’s main functionality. Members of the AUT’s development team do not necessarily need this, as the division of labor among the different teams is typically coordinated by the team leaders and a project manager. So a developer can focus on the part assigned to him and do it well even without having in depth knowledge of the whole system.
  2. Second, the automation scripts should be mapped to the test design, as they should emulate the steps done by a human tester. But, quite often, automation developers find out that the test design leaves many open issues that need to be resolved beforehand, as the automation scripts do not possess the flexibility and ingenuity of a human tester during runtime.
  3. Third, the GUI controls used by the development team, and also the AUT’s behavior may pose a real technological challenge regarding the identification of the objects by QTP (or other tools). Many times solutions that extend the basic capacities of the tools are required, as is the case with third-party and custom controls.

 

Following the above exposition, I think we may conclude that an automation project should be definitely managed as a development project. If this is so, why should one fall again and again victim to the fallacy of treating an automation project as a trivial task (does “record-replay” remind you of something?), where the opposite is true? Why not, then, approach to do it using the most widely accepted development method – Object Oriented – to attain the best possible results? In what follows I will describe a method to implement automation “scripts” based on an extension to the OR concept – the GUI Layer, which is based on solid OO principles.

 

The Concept of Layers

I hope to have managed so far to make clear why the OR approach, with scripts spread all around the place, is far from being the optimal approach. Now, following the above discussion, let us take a look at the concept of developing code in layers, especially the GUI Layer concept. In general, layers are useful to maximize reusability (recall the previous discussion on OO principles). I shall define a GUI Layer as a set of classes that pack (encapsulate) together the interfaces required to manipulate the AUT’s GUI objects in each application context. In other words, it is a set of classes that bridge between the AUT’s GUI (i.e., the test objects proper) and the Business or Application Layer, of which we shall have more to say later. In a sense, may be it would be more appropriate to call it the GUI-Business Bridge Layer, but it is already accustomed among the experts to call it a GUI Layer, to keep it short. I will illustrate below how such a layer can be built, and what are the benefits gained from adopting such an approach to testing automation.

Implementing the GUI Layer

Encapsulating Test Objects in Classes

Let us take a testing automation project on a typical AUT and see how the solution should be designed according to the approach outlined above. The first step would be to make a simple list of all application GUI contexts – i.e., the windows (pages in a Web application), dialogs, and popup messages. For each of these entities, which are containers of other GUI objects, we define a class, for example:

<span style="color: #0000ff;">Class</span> Login

<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> MainWindow

<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> CreateCustomer

<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

and so on for each of the application contexts. Because QTP does not allow for direct instantiation of classes defined in external vbs files with the operator New, it is also required to define the following function that will return an instance of a GUI layer class (a kind of constructor function), as follows:

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> CreateLogin()
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'Function: CreateLogin</span>
<span style="color: #008000;">\'Creates an instance of the Login class</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' Object - As Login</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #0000ff;">Dim</span> objLogin

    <span style="color: #0000ff;">Set</span> objLogin = <span style="color: #0000ff;">New</span> Login

    <span style="color: #0000ff;">Set</span> CreateLogin = objLogin
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>

The second step is, obviously, to define the members of each class. Now, since each class is, as aforesaid, a container of other GUI objects, we shall make use of the Scripting.Dictionary to store the references to the test objects contained in the window, dialog or page. (the  Dictionary object has been already extensively discussed in other articles published at AdvancedQTP‘s knowledge base). So, the first member I shall introduce here will be common to all GUI Layer classes, and I will define it as m_htChildObjects, for example:

<span style="color: #0000ff;">Class</span> Login
    <span style="color: #0000ff;">Private</span> m_htChildObjects <span style="color: #008000;">\'As Scripting.Dictionary</span>

<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> MainWindow
    <span style="color: #0000ff;">Private</span> m_htChildObjects <span style="color: #008000;">\'As Scripting.Dictionary</span>

<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

and so on for each of the application contexts (ht stands for HashTable, which is what the dictionary really is). The private member m_htChildObjects will be accessed through the class property ChildObjects. This property is defined as follows:

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'Property: ChildObjects</span>
<span style="color: #008000;">\'Get and Set the m_htChildObjects member field</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' R/W</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' dic</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' m_htChildObjects As HashTable</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Get</span> ChildObjects()
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #0000ff;">Set</span> ChildObjects = m_htChildObjects
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Let</span> ChildObjects(<span style="color: #0000ff;">ByRef</span> dic)
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #0000ff;">Set</span> m_htChildObjects = dic
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>

The third step is to define the objects contained within each context. For this purpose, I will define a public method within the class called Init, as follows:

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Init()
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'Function: Init</span>
<span style="color: #008000;">\'Initializes the context and child objects</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Dependencies:</span>
<span style="color: #008000;">\' IsContextLoaded(htContext)</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' True/False</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    ChildObjects = CreateObject(<span style="color: #006080;">"Scripting.Dictionary"</span>)

    <span style="color: #0000ff;">With</span> ChildObjects
        .Add <span style="color: #006080;">"Browser"</span>, Browser(<span style="color: #006080;">"name:=My App"</span>)
        .Add <span style="color: #006080;">"Page"</span>, ChildObjects(<span style="color: #006080;">"Browser"</span>).Page(<span style="color: #006080;">"title:=My App \- Login"</span>)
        .Add <span style="color: #006080;">"Username"</span>, ChildObjects(<span style="color: #006080;">"Page"</span>).WebEdit(<span style="color: #006080;">"html id:=Username"</span>)
        .Add <span style="color: #006080;">"Password"</span>, ChildObjects(<span style="color: #006080;">"Page"</span>).WebEdit(<span style="color: #006080;">"html id:=Password"</span>)
        .Add <span style="color: #006080;">"Submit"</span>, ChildObjects(<span style="color: #006080;">"Page"</span>).WebButton(<span style="color: #006080;">"outertext:=Submit"</span>)
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">With</span>

    <span style="color: #008000;">\'IsContextLoaded is a function that iterates through the Dictionary and checks if the GUI objects "exist"</span>
    Init = IsContextLoaded(ChildObjects)
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>

 

The code snippet above shows a typical Init method for a GUI Layer class for a Web application Login page. The test objects are added as entries to the ChildObjects Dictionary, and their identity is defined using Descriptive Programming (DP). The reader can easily infer the analogy to the OR. It is thanks to this encapsulation method that we ensure that GUI objects are always defined at a single place. At the end of the function body you will notice that it returns the result of a call to the function IsContextLoaded which accepts as argument the Dictionary that stores the ChildObjects.

IsContextLoaded is defined in a separate common library, as follows:

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> IsContextLoaded(<span style="color: #0000ff;">ByRef</span> htContext)
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'Function: IsContextLoaded</span>
<span style="color: #008000;">\'Checks that the current GUI context is loaded</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Iterates through the htContext (HashTable) items and executes the Exist method with 0 (zero) as parameter.</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' ByRef htContext - As HashTable</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' True/False</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' Meir Bar-Tal, SOLMAR Knowledge Networks Ltd.</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' 11-Nov-2008</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'See Also:</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #0000ff;">Dim</span> ix, items, keys, strDetails, strAdditionalRemarks

    <span style="color: #008000;">\'---------------------------------------------------------------------------</span>
    items = htContext.Items
    keys = htContext.Keys

    <span style="color: #0000ff;">For</span> ix = 0 <span style="color: #0000ff;">To</span> htContext.Count-1
        IsContextLoaded = IsContextLoaded <span style="color: #0000ff;">And</span> items(ix).Exist(0)
        strDetails = strDetails & vbNewLine & <span style="color: #006080;">"Object #"</span> & ix+1 & <span style="color: #006080;">": \'"</span> & keys(ix) & <span style="color: #006080;">"\' was"</span>
        <span style="color: #0000ff;">If</span> IsContextLoaded <span style="color: #0000ff;">Then</span>
            intStatus = micPass
            strDetails = strDetails & <span style="color: #006080;">""</span>
            strAdditionalRemarks = <span style="color: #006080;">""</span>
        <span style="color: #0000ff;">Else</span>
            intStatus = micWarning
            strDetails = strDetails & <span style="color: #006080;">" not"</span>
            strAdditionalRemarks = <span style="color: #006080;">" Please check the object properties."</span>
        <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">If</span>
        strDetails = strDetails & <span style="color: #006080;">" found."</span> & strAdditionalRemarks
    <span style="color: #0000ff;">Next</span>
    <span style="color: #008000;">\'---------------------------------------------------------------------------</span>

    Reporter.ReportEvent intStatus, <span style="color: #006080;">"IsContextLoaded"</span>, strDetails
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>

And it returns True if all objects defined in the Dictionary are identified, or False if at least one object is not found. This function is generic and it is used in the projects I manage to ensure that QTP does not get stuck while attempting to perform some operation on a non-existing GUI object. Another benefit of this method is that it points exactly to the object we need to recheck and update, making maintenance much easier.

Encapsulating Business Methods in Classes

The next step after defining the child objects of the GUI context is to define the operations required to perform the application or business scenarios within the given context. This is easily done by implementing class methods. For example, the login class outlined above would need the following methods to begin with: SetUsername, SetPassword and Submit. These are shown below:

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> SetUsername()
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'Function: SetUsername</span>
<span style="color: #008000;">\'Set the Username field</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Dependencies:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    ChildObjects(<span style="color: #006080;">"Username"</span>).<span style="color: #0000ff;">Set</span> GlobalDictionary(<span style="color: #006080;">"Username"</span>)
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> SetPassword()
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'Function: SetPassword</span>
<span style="color: #008000;">\'Set the Password field</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Dependencies:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    ChildObjects(<span style="color: #006080;">"Password"</span>).<span style="color: #0000ff;">Set</span> GlobalDictionary(<span style="color: #006080;">"Password"</span>)
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'------------------------------------------------------------------------------- </span>

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Submit()
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'Function: Submit</span>
<span style="color: #008000;">\'Presses the Submit button</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Dependencies:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' N/A</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    ChildObjects(<span style="color: #006080;">"Submit"</span>).Click

    <span style="color: #008000;">\'TODO: Verify data submission performed successfully</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>

Notice the use of the GlobalDictionary to retrieve the values required by the functions, and the use of the ChildObjects property to retrieve through the test object a reference to the runtime object.

The next step would be to move on to the Business Layer, which implements an application scenario building on the strong foundations of the GUI Layer. For instance to perform the Login function based on the above example, we could wrap it with the following function:

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> do_login()
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'Function: do_login</span>
<span style="color: #008000;">\'Implements the business logic of the do_login Action.</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Arguments:</span>
<span style="color: #008000;">\' None</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Returns:</span>
<span style="color: #008000;">\' Status</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #0000ff;">Dim</span> intStatus, objLogin

    <span style="color: #0000ff;">Set</span> objLogin = CreateLogin()
    <span style="color: #0000ff;">If</span> objLogin.Init() <span style="color: #0000ff;">Then</span>
        objLogin.SetUsername()
        objLogin.SetPassword()
        objLogin.Submit()

        <span style="color: #008000;">\'If login succeeds</span>
        intStatus = micPass
    <span style="color: #0000ff;">Else</span>
        intStatus = micFail
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">If</span>

    do_login = intStatus
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>

 

Notice the use the Login class outlined above and of its Init function as a precaution to ensure the GUI context is loaded, and not get stuck, as mentioned above. As you can also see, the code within the function above is easy to understand, and is not cluttered with the references to both the OR test objects and data sources as quite often is the case. If changes are made to the GUI objects of a given context, then the changes will be concentrated within a single package, both with respect to the object properties and to the functionality required to manipulate the context’s child objects. Yet another gain from this method is standardization. By implementing code this way we achieve a high degree of homogeneity in the code written by different developers, and thus enhancing the manageability of the automation project.

An advanced alternative to the last example is to pack such a Business Layer function using the Command Wrapper Design Pattern, as outlined in my article Function Pointers in VB Script (revised). For example:

<span style="color: #008000;">\'VB Script Document</span>
<span style="color: #0000ff;">Option</span> Explicit

<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">Class</span> do_login
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'Class: do_login</span>
<span style="color: #008000;">\'Encapsulates the do_login Action.</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Remarks:</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Owner:</span>
<span style="color: #008000;">\' John Doe</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'Date:</span>
<span style="color: #008000;">\' dd-MMM-yyyy</span>
<span style="color: #008000;">\'</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #008000;">\'Methods</span>
    <span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Default</span> <span style="color: #0000ff;">Function</span> Run()
    <span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #008000;">\'Function: Run</span>
    <span style="color: #008000;">\'Implements the business logic of the do_login Action.</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Remarks:</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Arguments:</span>
    <span style="color: #008000;">\' None</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Returns:</span>
    <span style="color: #008000;">\' Status</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Owner:</span>
    <span style="color: #008000;">\' John Doe</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'Date:</span>
    <span style="color: #008000;">\' dd-MMM-yyyy</span>
    <span style="color: #008000;">\'</span>
    <span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
        <span style="color: #0000ff;">Dim</span> intStatus

        <span style="color: #0000ff;">Set</span> objLogin = CreateLogin()
        <span style="color: #0000ff;">If</span> objLogin.Init() <span style="color: #0000ff;">Then</span>
            objLogin.SetUsername()
            objLogin.SetPassword()
            objLogin.Submit()

            <span style="color: #008000;">\'If login succeeds</span>
            intStatus = micPass
        <span style="color: #0000ff;">Else</span>
            intStatus = micFail
        <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">If</span>

        Run = intStatus
    <span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
    <span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>
<span style="color: #008000;">\'-------------------------------------------------------------------------------</span>

Adopting this method would enable the implementation of an advanced generic controller that loads its flow from an external data source such as an XML file. Such a controller device was developed by me together with my partners at SOLMAR Knowledge Networks as part of our generic Object Oriented comprehensive automation framework – My System.

Summary

This article has reviewed an alternative approach to code implementation in a testing automation project that is based on object-oriented principles. I have shown that automation scripting should not be treated differently than software development. Moreover, I tried to convey that logically following this line of thought leads to the conclusion that an automation project must be approached as a software development project par excellence, and suggested an expansion to the OR concept – which was shown as inadequate viz-a-viz the OO paradigm – to encapsulate the interfaces to the GUI objects: the GUI Layer. The article has provided a practical example of how to implement such a layer and how to invoke it using a Business Layer, and explained in detail the benefits of adopting this approach to obtain the maximum ROI from an automation project regarding maintainability, readability, scalability, extensibility and testability. Future articles will expand on this theme and guide the readers into how to gain from the implementation of other design patterns within the framework laid out in this article.

, , , , , ,

Adding a Call-Chain Mechanism to QTP

0
by on December 8, 2008 at 11:29

Background and motivation

A common frustrating experience is having your script pop-up an error dialog in some long-forgotten function, without one having the slightest clue in its regard. What usually follows is a series of trial-and-error breakpoint runs to determine which action the script was actually trying to perform when executing the function call that triggered the error.

All programming languages suffer from such problems, but in late bound languages like VB Script this problem is really challenging. This is because in VB Script compilation is done on the basis of a line-after line interpretation during run-time, in contrast to languages such as C/C++, in which compilation at least guarantees that the function calls are syntactically correct.

For example, a faulty call to a function (wrong function name or number of parameters) would pass unnoticed in VB Script when part of a function, whereas in C++ it would trigger a compilation error.

The only way to get the error message in such cases is by actually performing the code, that is, calling the wrapping function, or by testing the inner function call on a standalone basis. In any language, it is not long until the calls to functions and object methods become too deeply nested for us humans to grasp.

Having said that, programmers do debug their code, even when bugs and error occur within extremely nested function calls. They manage to do so via a dedicated mechanism: the call-chain (sometimes referred to as the call-stack).

The call-chain shows us exactly which function called the currently executing function, which function called that function, and so on up to the root of the call hierarchy. This allows us to analyze the error’s path, and see where things started to go wrong (usually 2-3 function calls before the function we are currently in).

An example of a Visual Studio call-stack for an error can be viewed in Figure 1 below. Please notice that it shows both the functions and code lines that precipitated the error.

clip_image002[9]

Figure 1: A Sample Visual Studio Error Call-Stack

 

Just imagine how many debug runs you could have saved just by knowing the exact order of calls which lead you to the current function. Or, how valuable would it been to know which parameters were sent into those function calls.

Possible Implementations

Now, it is possible to implement a call-chain in QTP with intensive reporting – just by sending a line to the report from any function as you enter and exit it. However, this will render your report cluttered and hence difficult to grasp. Moreover, QTP reports are inaccessible during the run-session, and that is exactly why the call-chain data is crucial.

Is the inevitable conclusion that there is no way to implement a call-chain in QTP effectively?

Well, the answer is no. There is a way, indeed, though it’s not trivial. A call-chain mechanism is just one of the rich features provided by SOLMAR’s flagship product: My SystemTM. Developed by our people at SOLMAR’s R&D Division, My SystemTM brings the latest advances to boost ROI on testing automation (We shall have much more to say on that in future posts; stay tuned!). Meanwhile, I thought that I could share some of my techniques and thoughts on the subject.

Though the examples given here are not meant to be used or to be an out-of-the-box solution, I do believe they can point you in the right direction for developing robust solutions of your own.

 

The Concept

Developing an effective call-chain requires four basic components:

1. A place to store the call-chain data.

2. A mechanism for updating the call-chain when you enter a context (by context I mean a function, sub or action).

3. A mechanism for updating the call-chain when you exit a context.

4. A mechanism for viewing the stored data mid-run (we won’t get into this one – it is basically just a simple window to peek at the stored data)

If you have a robust mechanism for updating the call-chain when you are exiting and entering contexts, and an easy to use storage for the call chain, then implementing the call-chain will be quite straightforward.

 

Storing the Call-Chain Data

An excellent mechanism for storing call-chain data is a stack. A stack can store data (i.e., the names of the contexts you happen to enter during the flow) in a FILO manner – First-In Last-Out. This will make sure your call chain contains all the relevant data needed to understand what got you until this point, and nothing else. Here is an example of how the stack mechanism would work:

Let’s say our code went though these functions:

<span style="color: #0000ff;">Function</span> Func1
    <span style="color: #0000ff;">Call</span> Func2
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

<span style="color: #0000ff;">Function</span> Func2
    <span style="color: #0000ff;">Call</span> Func3
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

<span style="color: #0000ff;">Function</span> Func3
    <span style="color: #0000ff;">Dim</span> x

    <span style="color: #0000ff;">Call</span> Func 4
    x = 1/0
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

<span style="color: #0000ff;">Function</span> Func4
    <span style="color: #008000;">\'Ended</span>
<span style="color: #0000ff;">End</span> Function

If we call Func1, it will call Func2, which would call Func3, and it would call Func4. At this point, our call-chain stack would look like this: (“Func1”, “Func2”, “Func3”, “Func4”). Now Func4 will end, returning the control to Func3. Since our call chain is a Stack, once the code will exist Func4, it will remove the last item from our stack, make it: (“Func1”, “Func2”, “Func3”).

Line 14 will result is an error, where we could debug, and take a look at our stack, which would hold just enough data to understand how we got into Func3, and no irrelevant data (for example, the fact the we have entered and exited Func4).

If you’d like to know exactly how to implement a stack, you can take a look at Dani Vainstein’s excellent article on the matter – here.

 

A Naive Approach to Updating the Call-Chain Data on the Run

Well, after we got our own storing mechanism for the call-chain data, we better start recording it. The naive approach would be to simply perform an add command once we enter a function, and a remove command every time we exit it. So it should look something like this (assuming our data is stored in an oChainData stack storage, with add and remove commands):

<span style="color: #0000ff;">Function</span> Func1
    <span style="color: #008000;">\'Add to call chain</span>
    oChainData.Add <span style="color: #006080;">"Func1"</span>

    <span style="color: #008000;">\'[...</span>
    <span style="color: #008000;">\' Function body</span>
    <span style="color: #008000;">\' ...]</span>

    <span style="color: #008000;">\'Remove from stack</span>
    oChainData.Remove
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

Notice that we don’t have to specify the function name for our Remove command, as the stack mechanism always know to remove the top level item.

 

Problems with the Naive Approach

That is all fine and well, but usually our code is more complex than this – usually our functions have multiple exit gates, as shown in the following example:

<span style="color: #0000ff;">Function</span> Func1
    <span style="color: #008000;">\'Add to call chain</span>
    oChainData.Add <span style="color: #006080;">"Func1"</span>

    <span style="color: #008000;">\'Code...</span>
    <span style="color: #008000;">\'If structures etc...</span>
    oChainData.Remove
    <span style="color: #0000ff;">Exit</span> <span style="color: #0000ff;">Function</span>

    <span style="color: #008000;">\'More Code...</span>
    <span style="color: #008000;">\'If structures etc...</span>
    oChainData.Remove
    <span style="color: #0000ff;">Exit</span> <span style="color: #0000ff;">Function</span>

    <span style="color: #008000;">\'More Code...</span>
    <span style="color: #008000;">\'If structures etc...</span>
    oChainData.Remove
    <span style="color: #0000ff;">Exit</span> <span style="color: #0000ff;">Function</span>

    <span style="color: #008000;">\'Now the regular exit gate:</span>
    oChainData.Remove
<span style="color: #0000ff;">End</span> Function

When our function has multiple exit gates, it is just a matter of time before we forget to update the call-chain, and that would just make our whole stack misleading and useless.

And even if our code is perfect (and it never is), we still run the risk of a mid-function error that will throw us out, before letting us update the call-chain data. But such errors are exactly our motivation to be willing to put an effort in maintaining a call-chain mechanism in the first place!

 

A More Robust Solution

We have seen that our naive solution fails at some basic everyday cases, but perhaps we can overcome this with more complex scripting.

What these examples show is that we need an automatic component that will “listen” to our function, and tell us (or rather the call-chain) when we enter and exit it. Implementing the whole package (monitor both entering and exiting a function) might be hard, but we can do the exit part quite easily.

In order to achieve that, we could use the termination event of an object. When we write a class, anything we write within the Class_Terminate sub of the class will be executed when the class object is destroyed. So, for example:

<span style="color: #0000ff;">Class</span> KillSwitch

    <span style="color: #0000ff;">Sub</span> Class_Terminate
        Msgbox <span style="color: #006080;">"Object terminated"</span>
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Set</span> oKill = <span style="color: #0000ff;">New</span> KillSwitch
<span style="color: #0000ff;">Set</span> oKill = <span style="color: #0000ff;">Nothing</span>
\'Now the message box will appear

As you can see, the object “knows” when it is destroyed. We can use this to our advantage – instead of popping up a message box, let us make our object update the call-chain. Since the object will be destroyed whenever we exit the function (either purposefully, or due to an error), we don’t have to worry about “forgetting” to update the call-chain. It could look something like this:

<span style="color: #008000;">\'The blueprint for the updater</span>
<span style="color: #0000ff;">Class</span> ChainUpdater

    <span style="color: #0000ff;">Sub</span> Class_Terminate
        oChainData.Remove
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Function</span> Func1
    <span style="color: #0000ff;">Dim</span> oUpdater <span style="color: #008000;">\'This will hold the exit kill-switch</span>

    oChainData.Add <span style="color: #006080;">"Func1"</span> <span style="color: #008000;">\'Update the chain-data</span>

    <span style="color: #0000ff;">Set</span> oUpdater = <span style="color: #0000ff;">New</span> ChainUpdater <span style="color: #008000;">\'Create an instance of the kill switch</span>

    <span style="color: #008000;">\'Now, no matter when we exit the function, the kill switch will be activated</span>
    <span style="color: #008000;">\'And the chain-data will be updated</span>

<span style="color: #0000ff;">End</span> Function

Summary

This article introduced the concept of a call-chain and provided substantial reasons for its centrality in the automation development effort. Although the article did not provide a detailed example of a call-chain, I hope that it has successfully outlined how to build the main components that are required in order to implement such a mechanism.

The main concept explained how we can keep track of the chain-data. We have seen that the naive approach (manually update on entering and exiting a context) is impractical. We examined an alternative approach by which call-chain update is achieved automatically through an object’s kill-switch mechanism.

As aforementioned, SOLMAR Knowledge Networks has developed an automation framework with a built-in call-chain mechanism. While it is much more detailed and effective, it builds upon the same basic principles demonstrated in this article.

As a closing word, let me encourage you to share your thoughts and comments about the call-chain concept, about this article, and if you happen to dare and design, to share with the community your own call-chain mechanism!

, , ,

Class Inheritance in VBScript – Now at your Doorstep!

0
by on October 15, 2008 at 11:33

ChangeLog:

5/10 : I’ve uploaded a new file with the fixed parser. Added support for code blocks with sensitive names (Function PrivateFunciton, Sub SpecialGet etc.)

10/10 : I’ve uploaded a new file with constructor arguments support, and a new regular expression engine (preserves comments and should make things go faster).

Notice that the new file requires a new run command (use NewRegExParser.dll instead of ClassParser.Dll).

To use constructor arguments:

  1. In your Class_Initialize sub, add parameters with a comment. For example: Sub Class_Initialize ‘(Param1, Param2)
  2. When you create a new instance of your class, use comments to deliver values to these parameters. For example Set x = new MyClass’(5, “Something”)

You can pass parameters and variables. However, these will always be passed as ByVal.

 

The Problem

One of QTP’s biggest downsides is its VBScript engine. While it may answer most of your everyday needs, it falls short when it comes to classes and object-oriented programming.

On the top of my personal VBScript missing features list is the ability to perform class inheritance, which could save tons of duplicate code, and significantly reduce maintenance overheads. Here’s a quick example for class inheritance. Imagine this is a basic “engine” template:

<span style="color: #0000ff;">Class</span> AbstractEngine
    <span style="color: #008000;">\'This will include general methods, variable and properties</span>

    <span style="color: #0000ff;">Private</span> pEngine    <span style="color: #008000;">\'Each engine will hold its object here</span>
    <span style="color: #0000ff;">Private</span> pFileName    <span style="color: #008000;">\'The location for the output</span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Get</span> File
        File = pFileName <span style="color: #008000;">\'This is the same for all engines</span>
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Sub</span> StartEngine
        <span style="color: #008000;">\'Each engine should implement this on its own</span>
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Sub</span> Report(sEventName)
        <span style="color: #008000;">\'This is the same for all engines</span>
        pEngine.Report sEventName
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

<span style="color: #0000ff;">End</span> Class

We would like to write specific engines that would implement this more general class template. In every other language, we could’ve inherited the AbstractEngine, and immediately reuse its code without having to rewrite it specifically in our own “derived” class. This would allow us to maintain the “base” code in only one place – the abstract class, and all the derived class would immediately inherit our repairs and changes.

However, as I’ve noticed, VBScript does not support class inheritance, thus forcing us to manually copy the base code to all our derived classes. This means we would have to duplicate changes in multiple files, projects and computers – severally hampering our automation ROI.

The solution

I’ve prepared an initial solution, which will allow you to load VBScript library files to QTP, while performing inheritance parsing for all the classes in all the library files. This will effectively allow you to write your files in a compact, efficient manner, while enjoying all the benefits of true class inheritance. Currently there is a major downside though – the technique does not support runtime debugging (as it relays on ExecuteGlobal to inject the parsed code into QTP).

Here’s an example of what you can expect. The input would be:

<span style="color: #0000ff;">Class</span> GrandParent
    <span style="color: #0000ff;">Private</span> Private_Variable

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Grand(sName)
        Grand = sName
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> Parent
    <span style="color: #008000;">\'<Inherit> GrandParent</span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Grand(sName, sNewVariable)
        Grand = sNewVariable
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

    <span style="color: #0000ff;">Private</span> <span style="color: #0000ff;">Sub</span> SomethingNew
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Get</span> Name
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> Child
    <span style="color: #008000;">\'<Inherit> Parent</span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Set</span> Name
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Grand
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #0000ff;">End</span> Class

And the automatically generated output you’ll get will look like this:

<span style="color: #0000ff;">Class</span> GrandParent
     <span style="color: #0000ff;">Private</span> Private_Variable

     <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Grand(sName)
         Grand = sName
     <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> Parent
     <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Grand(sName, sNewVariable)
         Grand = sNewVariable
     <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

     <span style="color: #0000ff;">Private</span> <span style="color: #0000ff;">Sub</span> SomethingNew
     <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

     <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Get</span> Name

     <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>

    <span style="color: #0000ff;">Private</span> Private_Variable
    <span style="color: #008000;">\'Inherited from GrandParent </span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> MyBase_Grand(sName)
    <span style="color: #008000;">\'Inherited from GrandParent </span>
         MyBase_Grand = sName
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

<span style="color: #0000ff;">Class</span> Child
     <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Set</span> Name
     <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>

     <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> Grand
     <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> MyBase_Grand(sName, sNewVariable)
        <span style="color: #008000;">\'Inherited from Parent </span>
         MyBase_Grand = sNewVariable
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>

    <span style="color: #0000ff;">Private</span> <span style="color: #0000ff;">Sub</span> SomethingNew
        <span style="color: #008000;">\'Inherited from Parent </span>
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Sub</span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Property</span> <span style="color: #0000ff;">Get</span> Name
        <span style="color: #008000;">\'Inherited from Parent </span>
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Property</span>

    <span style="color: #0000ff;">Private</span> Private_Variable
    <span style="color: #008000;">\'Inherited from Parent </span>

    <span style="color: #0000ff;">Public</span> <span style="color: #0000ff;">Function</span> MyBase_MyBase_Grand(sName)
         <span style="color: #008000;">\'Inherited from Parent </span>
         <span style="color: #008000;">\'Inherited from GrandParent</span>
         MyBase_MyBase_Grand = sName
    <span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Function</span>
<span style="color: #0000ff;">End</span> <span style="color: #0000ff;">Class</span>

Analysis

Let’s see what’s going on here: You can mark classes as derived classes by inserting a specially constructed comment of the form ‘<Inherit> ParentClassName. You can even set up multiple inheritance if you’d like (even though this example doesn’t make use of multiple inheritance).

The GrandParent class was not marked as derived, so it’s copied fully to the output, without any variations.

The Parent class was marked as derived from GrandParent, so it’s supposed to inherit all of GradParent’s properties, methods and variables. It received the variable Private_Variable, which is copied from GrandParent as a whole. Parent already had a function named Grand, which effectively overrides GrandParent’s more general Grand function. So GrandParent’s function is renamed into “MyBase_Grand” function (allowing the override, while not losing the inherited data).

This behavior is true for functions, subs, and properties (but not for simple variables). Notice the autoparsing engine has also updated the function’s inner structure, to reflect the name change (instead of the line Grand = sName, it’s MyBase_Grand = sName).

The Child class was marked as derived from Parent. The parser first works on Parent to make sure it has inherited everything it needs to, and then it proceeds to copy the relevant code segments into Child. Child’s Name property has only a Set segment, so the Get segment is copied from Parent (the parsed can tell between Get, Let and Set segments). The SomethingNew sub is copied without any variations, as does the Private_Variable variable. As child includes a Grand function of its own, the inherited functions receive a MyBase_ prefix to their names, as seen before.

Update – Constructor Arguments

Most programming languages allow you to pass parameters when you create an instance of your class. These are called constructor arguments. VBScript’s constructor (the Class_Initialize sub) can not receive parameters, so usually we must resort to solutions as a “Build” or “Start” sub, in addition to the actual constructor, in order to relay the parameters.

Well, No more! The class parser allows support for constructor arguments in the following way:

1. Update your Class_Initialize Sub by adding parameters in a comment right after the sub’s header. For example: Sub Class_Initialize ‘(Param1, Param2, SomeCrazyName).

The parameters can have any legal name you’d wish them to have. Do not use ByVal or ByRef prefixes (the parameters will always be ByVal).

2. Use the same comment method to pass parameters to the constructor when you create a new instance of your class.

For example: Set oMyClass = New clsMyClass ‘(5, “Something”, SomeVariable)

You can either pass the relevant value directly (e.g. 5, “Something”), or use an existing variable (e.g. SomeVariable).

The parameters will be transported into the Class_Initialize sub, where you can use them as usual.

For example: Msgbox (Param1).

Usage

In order to use the parser, download it from here, and extract it to your preferred location.

Then, REMOVE all you class library files from your test (i.e. delete the from the resource tab), and instead, add a new library file with the following content:

<span style="color: #0000ff;">Dim</span> oClassParser

<span style="color: #0000ff;">Set</span> oClassParser = DotNetFactory.CreateInstance(<span style="color: #006080;">"NewRegExParser.Parser"</span>, <span style="color: #006080;">"<File Location>"</span>)

oClassParser.LoadFile(<span style="color: #006080;">"<LibFile1>"</span>)
oClassParser.LoadFile(<span style="color: #006080;">"<LibFile2>"</span>)

ExecuteGlobal oClassParser.Context

Where <File Location> is the location to the NewRegExParser.dll file, and <LibFile1>,2,3… are the locations to the library file you’ve deleted.

QTP will create the parser, load the files into it, parse them, and will hand you the output through the .Context Property. Throwing the output into the ExecuteGlobal command will load it into QTP, and will make its classes, functions and variables usable from within the script.

 

Summary

VBScript’s lack of class inheritance can be quite problematic for those of us who are using Classes regularly throughout our scripts. The attached file provides a parser which enables you to effectively use class inheritance in VBScript by placeing ‘<Inherit> ParentClassName comments within your classes. The mechanism will:

1. Enable inheritance from multiple parents (just use multiple lines with Inherit comments)

2. Copies Properties, Variables, Functions and methods from the parent class to the derived class.

3. In case the derived class already has a code segment with the same name, the copied code-segment is changed to MyBase_<Name>. Inner segment placements are updated accordingly.

4. Enables inheritance through multiple hierarchies (e.g. GrandParent->Parent->Child inheritance). Circular inheritance protection. The order of appearance in the files is irrelevant, the parser will make sure the parent classes are parsed before their derived classes.

5. Enables passing arguments to Class_Initialize constructors by using structured comments.

6. The parser has a major downside – as the technique uses ExecuteGlobal to load the processed classes into QTP, you cannot debug the code in mid-run.

Reliability

I’ve tested the parser on a wide variety of code libraries, and it seems to be working fine. Having said that, there’re probably some undocumented bugs which might prove to be catastrophic – be warned.

I’ve not planing on actively maintaining the parser, but if you’ll report bugs that are easily fixed, I’’ll work on them and post repaired versions when I get the chance. The parser is distributed AS IS, with NO WARANTIES, and should be regarded as a Beta / Alpha release.

Future Releases

This is just a preliminary work on the way to an integrated QTP parser. The end goal is being completely transparent for the user. Pressing F5 will run the test as usual, and the parser will work in the background in a manner which will allow debugging though the run-session.

I already have archived some milestone toward this goal – If you’d like to participate in the product’s beta group, please contact me and say so. Not everyone will be able to participate – so don’t get your hopes up :) .

,