«

»

Mar 29 2008

Introduction to Code Design

This will be the first of many articles dealing with code design. What is code design? In object oriented languages it sometimes stands for deciding on the object class and structures needed to elegantly complete a task. For us it will just be a set of abstract principles that will allow us to write readable, maintainable code.

Among the QTP programmers community, many think that designing code is a waste of time. This view is common for many reasons, especially because QTP uses VBScript, which is a degraded programming language, and doesn’t support many of the prerequisites for code-design. However, some code-design core principles can be done in VBScript, and are the only way to keep our QTP code readable and maintainable over time.

Talking about code design concepts can be very confusing. Many of the concepts overlap, and sometimes one concept will be referred to by different names. I’m going to make things worse by merging some concepts, degrading and chopping parts of others, in order to make them more relevant to VBScript and QTP.

So, if you have any programming experience (especially with OO languages), you’ve probably heard these terms being used differently. Just remember I’m trying to keep my focus on QTP and VBScript, sometimes at the expense of accuracy (and this is only one of many articles that will expand and elaborate).

Today we’re going to deal with three main concepts: encapsulation, black-box coding and separation of concerns (SoC). These concepts have many synonyms, and sometimes overlap and refer to the same basic things, so demonstrating each of them separately is quite impossible. I’ll do a quick run through all of them, and then demonstrate them with a single test-case.

Keeping these principles in mind while writing our code will have an enormous impact on our future maintenance, code readability, bugs, and ability to add more features and functionality to out scripts. So pay attention :)

Separation of concerns

Is the best place to begin – it is simple, natural, and chances are you’re already doing it (to some degree). Simply put, it’s about breaking the code into as many logical pieces as you can, with as little overlapping as possible. This is usually done by separating long pieces of code into small dedicated functions. Later, the code looks like a composed puzzle, or a Lego combination of the different logical building blocks.

By packing each logical “block” separately, we achieve several goals:

Our code is much more readable

We can later maintain each of the blocks separately

The process can help us locate other places that use the same logical block, and turn the duplicate code into function calls

And many more benefits…

Without SoC, you have many different logical tasks intertwined throughout the code. If one of these tasks will need to change in the future, you’ll be force to fix the code in many different places, while risking messing up the other tasks’ functionality. With SoC, you can touch each logical task on its own, knowing that there’s no risk bugs will raise in different parts of your code.

So to illustrate: Dealing with warnings and error messages can be broken into three parts (actually more, but let’s go with 3):

Dealing with the message container

Recognizing the message

Reacting to it

If your code doesn’t separate recognizing the message from the actions needed to react to it, things can get very ugly very soon. What will happen when a new error message is introduced? Are you sure you know ALL the places that need updating? Can we really change the reactions without affecting message recognition? What if dealing with the new message requires performing actions in a different window? How will you construct a maintainable if switch?

Typically, in order to cover all our bases, we must put a hefty douse of duplicate code, or work very hard to tackle every little change in the future. Either way, it sucks, and our code will look terrible: it will be hard to tell which commands react to the error and which deal with the pop-up; it won’t be clear how the if-structure plays out; and the whole thing will barely be readable.

Black Box Coding

AKA concealing, decoupling or implementation hiding – is a concept that helps us to better build the separate building blocks we produced in order to keep our separation of concerns. The concept further deepens the separation and independents of the blocks, ensuring we could radically change any one of them, without any impact on the rest of the script.

The name actually says it all – the pieces of the script should operate as black-boxes. No one outside of the function or the action should know HOW it achieves its goal (its implementation), only how to tell her to do so. In a certain way this means we should “dumb down” the script, so it just calls all the smart folks to do the actual work for it. Actually, even these folks are dumb, and call other folks to do some of the work for them.

Without back-box coding, changes in one function will require changes is many other places in the code (because they’re exposed to the function’s implementation). Because it’s impossible to remember all the places that are relevant for a code-fix, we will usually end up with many small bugs that will either fail, or mess around with our script.

Unfortunately, no matter how hard we try, VBScript forces us to changes our code when the function’s signature (parameters) change – more on that here. But other than that, you should strongly avoid any kind of dependencies between different black-boxes in the script.

For example: Writing to the log

Bad way: insert Reporter.ReportEvent commands straight into the code

This exposes the implementation of the log-reporting, which means it will be impossible to change the means of reporting without changing each and every reporting command throughout the entire script. This is what we call strong-coupling – the log-reporting functionality is coupled with other parts of the script in an inseparable way.

Good Way: wrap the Reporter command with a custom function, and call it from the script:

Function QTPReport(iStatus, sHeader, sContent)
   Reporter.ReportEvent iStatus, sHeader, sContent
End Function

Here we’ve decoupled the logging into a black-box. No one knows HOW it logs, but whenever the script needs logging services, it just calls the black box, knowing that it will get the job done. If we’d like, we could switch to excel logging by changing this function only, and the rest of the script will require zero-maintenance.

Encapsulation

Is all about gathering all the code that’s responsible for a specific task under one roof. The concept biggest benefit is eliminating any duplicate code in our scripts, but there are many welcomed side-effects. When there’s only one “parent” responsible for each task, the code becomes much clearer, and easier to maintain and expand.

When our logical functionality is properly encapsulated, there are no “I hope we fixed all the places that did X”, because X is done in one place only. When you’ll find a bug, it will usually take you less then two seconds to find the exact place to fix it. Expanding functionality is done decisively and quickly, in case where it would be considered impossible in the past.

Summary

I hope you’ve got the feeling that these concepts overlap and complete each other. When you separate concerns, you’ll be doing so in black-boxes. And when you’re building a black-box, it will be properly encapsulated. And you can’t encapsulate anything without separating it from other logical functionality (SoC), and making it into a black-box. Well, you got the point.

The bottom line is writing maintainable code, by reducing the dependencies between the different parts. This way we can expand, fix, and maintain each part, without having to worry about the rest of the script. These concepts are so basic; the almost any technological design has to take them into account – in order to make a car maintainable, you got to make sure that changing a light bulb won’t affect the steering. We’ll see how these and other concepts play their parts in the coming articles.

Example

To make these concepts more tangible, here is an example of an error handling approach which utilizes these concepts. The example makes use of DP and dictionary objects, so make sure you know them well enough. Of course this isn’t the best way to deal with error messages, but it would do to demonstrate these concepts.

Download the example-app, cut and paste the code to QTP, and run it to see the results (don’t forget to change the AppPath constant at the top!).

The script will open the app, initiate an error window, and send it to the central function dealing with errors. The function will use its delegate-functions to extract the error from the window; analyze it and determine the needed actions to resolve it; and execute these actions.

Notice that if a new error message would be introduced (or an old one to be changed, we will only need to add a recognition block to the analyzer function (or edit an existing block), and within that block, add needed actions to be executed.

As with many of my examples, I try to introduce new ideas and concepts, beside those specifically discussed in the article. In this example you’ll find, among other things, using strings to represent action instructions, and using funcitons as factories that process these instructions and executes them.

'Where the app. file was saved
Const AppPath = "c:\" ‘Dont forget to change me!

'Gets the error message 
Function ExtractErrorMessage(oErrorWindow)
  Dim sMessage
  Dim sWindowType

  'we’re assuming we can tell the windows apart by the window class.
  'More conditions and cases could be added 

  sWindowType = oErrorWindow.GetROProperty("micclass")
    Select Case sWindowType
      Case "Dialog"
        sMessage = oErrorWindow.Static("micclass:=static").GetROProperty("text")
      Case "VbWindow"
        sMessage = oErrorWindow.VbLabel("vbname:=Label1").GetROProperty("text")
      'More cases go here
    End Select

    ExtractErrorMessage = sMessage
End Function

'analyzes the message, and spits out a collection of needed actions to resolve it
'Very complex action instructions can be created via nested dictionaries

Function AnalyzeMessage(sMessage)
  Dim oNeededActions
  Dim bFound

  'The actions collection
  Set oNeededActions = CreateObject("Scripting.Dictionary")
  bFound = False ' Keep searching for a match?

  'Recognition Block
   If (instr(sMessage, "Index was outside the bounds") > 0) Then
        bFound = True
        oNeededActions.Add "Log", "Warning : " & sMessage & vbcrlf & "Tring to proceed"
        oNeededActions.Add "Close Error Window", "No Data" ‘Just to demonstrate
   End If

   'Recognition Block
    If (bFound = False) And (instr(sMessage, "Null") > 0) Then
        bFound = True
        oNeededActions.Add "Log", "Error: " & sMessage & vbcrlf & _
            "Killing app and restarting"
        oNeededActions.Add "Kill App", "Code Design.exe"
        oNeededActions.Add "Open App", AppPath & "Code Design.exe"
        oNeededActions.Add "Press", "Trigger 1"
        'In real life, the data could be the QTP ref to the button
    End If

    ' More Recognition Block
    'Can use regular expression, complex If structures, etc.

    oNeededActions.add "Result", bFound 'Did we manage to analyze the error?

    Set AnalyzeMessage = oNeededActions
End Function

'Just an example of a function that executes commands
'More advanced uses could be achieved via Execute command

Function ExecuteNeededActions(oNeededActions, oErrorWindow)
  Dim bResult
  Dim i
  Dim arrActions
  Dim arrData

  bResult = True

  'See QTP help on Dictionary Objects for these lines
  arrActions = oNeededActions.Keys 'The names of the actions - array
  arrData = oNeededActions.Items 'The data of the actions - array

  For i = 0 to uBound(arrActions) 'Loop through actions
    Select Case arrActions(i)
      Case "Kill App"
        SystemUtil.CloseProcessByName arrData (i)
      Case "Log"
        Reporter.ReportEvent micWarning, "Got error message", arrData(i)
      Case "Open App"
        SystemUtil.Run arrData(i)
        wait 1
      Case "Close Error Window"
        oErrorWindow.Type MicEsc
      Case "Press"
        VBWindow("vbname:=AdvancedQTP_Code_Design")._
          VBButton("text:=" & arrData(i)).Click

     'Just for the kicks of it, rerun the whole thing for the second message
      DealWithError(VBWindow("vbname:=AdvancedQTP_Code_Design")._
        Dialog("micclass:=Dialog"))
      End Select
  Next

  ExecuteNeededActions = bResult
End Function

'The main function for dealing with errors
'An error could raise in different windows, all will be sent here

Function DealWithError(oErrorWindow)
  Dim sMessage
  Dim bResult
  Dim oNeededActions

  bResult = True

  'get the message 
  sMessage = ExtractErrorMessage(oErrorWindow)

  If sMessage = "" Then
    Reporter.ReportEvent micFail, "Failed to get message", "Aborting"
    DealWithError = False
    Exit Function
  End If

  'Determine needed actions
  Set oNeededActions = AnalyzeMessage(sMessage)

  If oNeededActions("Result") = False Then
    Reporter.ReportEvent micFail, "Coud not analyze message", sMessage
    DealWithError = False
    Exit Function
  End If

  'Execute the actions
  bResult = ExecuteNeededActions(oNeededActions, oErrorWindow)

  DealWithError = bResult
End Function

'Open App

SystemUtil.Run AppPath & "Code Design.exe"

wait 1

'Initiate Error
VBWindow("vbname:=AdvancedQTP_Code_Design").VBButton("text:=Trigger 2").Click

'Lets assume a recovery scenario has catched the window, and sent it to our funciton
DealWithError(VBWindow("vbname:=ErrorMessage", "index:=0"))

About Yaron Assa

A leading automation expert, Yaron is the original founder of AdvancedQTP.com, and also ex co-founder and CTO of SOLMAR Knowledge Networks Ltd. Yaron has his own website, and also runs a Hebrew podcast on skeptical thinking and science (http://www.safeksavir.co.il).