Introduction to LGuiTest

by Piotr Kaluski

Introduction

This document describes the main concepts of LGuiTest perl module. It provides an object oriented framework, which supports writing complex GUI test automation scripts in a maintainable and robust way. Basic functionality is presented in the form of an example.

Prerequisites

In order to run the example you will need
It is also desirable that you are familiar with the basic concepts of GUI automation on Win32. The tutorial on Win32::GuiTest should be sufficient.

Getting the right version of Win32::GuiTest

You should be able to get the most recent version of Win32::GuiTest from its Source Forge project site. Unfortunately, this is not always the case. As of this writing i.e. 15.01.2005 the most recent version is available here. But I hope that sooner or later Source Forge will become the main source of most recent distributions.
Whatever the place you took it from, in order to have all the example code working you need to get Win32::GuiTest with a bug fix. The fix is not available in the distribution so I placed the fixed copy of the module here. It is a ppm module, so it should be easy to install. I will try to have this changes incorporated in the main distribution.

If you are curios what was the bug read on.
Function FindWindowLike has the following parameters:
For most of this parameters, passing "undef" means "all". So passing "undef" as $childid will force FindWindowLike to match window with any control id. However, if you pass 0 (zero) as a $childid, FindWindowLike will behave as if it got "undef". This is wrong, since it is quite common to find a control with control id equal 0.
Below you can find a diff of a fixed version GuiTest.pm file:

C:\Perl\site\lib\Win32>diff -c guitest.pm guitest_old.pm
*** guitest.pm  Sun Jan 16 17:23:36 2005
--- guitest_old.pm      Thu Dec 23 12:49:44 2004
***************
*** 385,394 ****
              (!$classre  || $sClassname =~ /$classre/))
          {
              DbgShow("Matched $1\n") if $1;
!             if (not defined $ID) {
                  # If find a match add handle to array:
!                 push @found, $hwnd;
!             } elsif (defined $sID) {
                  if ($sID == $ID) {
                      # If find a match add handle to array:
                      push @found, $hwnd;
--- 385,394 ----
              (!$classre  || $sClassname =~ /$classre/))
          {
              DbgShow("Matched $1\n") if $1;
!             if (!$ID) {
                  # If find a match add handle to array:
!               push @found, $hwnd;
!             } elsif ($sID) {
                  if ($sID == $ID) {
                      # If find a match add handle to array:
                      push @found, $hwnd;




Getting the right version of Win32::LGT (LGuiTest)

Win32::LGT is a module in alpha state. It is included in the package with all the code and text files used in the example described below. Documentation is poor, there are probably many bugs. What I can tell you is that I am using LGT at work and it helps me in handling windows with pretty complex combination of controls.

A note on using Internet Explorer as an example

I am using Internet Explorer as an example application. There are 2 main reasons for doing this. It is common - if you have MS Windows, you have IE. Secondly, it has some interesting hierarchy of controls and subwindows. This hierarchy is a good illustration of  problems you may encounter during writing automation scripts using Win32::GuiTest.
IE is OLE enabled, so for many people, using Win32::GuiTest instead of OLE may seem artificial. And they are probably right. But for the reasons given above, IE is well suited for being illustration of issues discussed in this tutorial.
As we well know, Microsoft loves changing things dramatically. It may occur that the tutorial will not work with IE 7.0. Therefore, I think that the best way to make this tutorial valid for a longer time, would be to create a simple window application, which would serve as an example. If you can help in creating such an application, let me know at pkaluski@piotrkaluski.com. I believe that for an experienced Windows developer it is going to be a piece of cake.

Terminology

In order to avoid confuses, I will use the term Win32 in order to refer to WinNT/2K/XP operating system.

What is LGuiTest, why is it needed.

In short, LGuiTest (LGT) is a Perl module for GUI automation. It is based on Win32::GuiTest module. It is obvious what are modules like Win32::GuiTest for, therefore it is also easier to explain and to understand how to use them. I am under the impression, that the need for features provided by LGuiTest is not that obvious. Hence, I believe it will be much easier to understand what is LGuiTest, if I explain what made me creating it.
As have I said above, Win32::GuiTest is a module for a GUI test automation. You can find a simple introduction to this module here. It provides a still growing set of functions for manipulating windows in Win32 operating system. Using those functions we can simulate mouse clicks and keys pressed, hence, we can create scripts, which will automatically perform actions, which in normal circumstances will be done by human with a keyboard and with a mouse.
I started to use Win32::GuiTest in my job. I had to automate tests of application written in VC++. The windows' hierarchy in this application was pretty complicated i.e. there were many levels in the windows tree. Simple edit boxes were grand-grand-...-grand children of the main window. As a result, accessing them (getting their window handles) required iterating through all their parents. I have quickly observed that the code becomes really hard to maintain. Moreover, simple change in the windows' hierarchy, resulted in the necessity of relatively big changes in the code. So I started to think about the possible way of solving this problem. My conclusion was, that the solution should satisfy the following requirements:
By business level I understand the organization of GUI from the user's point of view. Technical level is an organization of GUI from the point of view of the operating system.
The person writing the testing script should not have to care too much about the windows' hierarchy. He/she should only care about giving values to proper controls. Example: Internet Explorer has an url input window, named "Address" (see picture).

IE Address control
Address control in Internet Explorer


 It is a 5th level descendant of the main window, which is shown by WinSpy.

Windows hierarchy
Address control in windows' hierarchy


But from the point of view of a user, it is a child control in the main window. The person writing the automation script should refer to "Address" instead of "main_window.WorkerWindow.ReBar.ToolBar.Combo32.ToolBar.Address".

This means that if we change the windows' hierarchy tree, the testing script should remain unchanged, or changes should be small.

LGuiTest is aimed to satisfy those two requirements. Read further to see how is it done. I will illustrate it by the example.

How LGuiTest works

Let's start with example

Let's imagine that we would like to write a simple test for Internet Explorer. We want to do 2 things - insert some text into an "address" window and then do some interesting mysterious things with its menu bar.
Let's start with the "Address" field. With Win32::GuiTest we can achieve this with the following code:

     1    use Win32::GuiTest qw( FindWindowLike SendKeys SetForegroundWindow );
     2    use strict;
     3    
     4    #
     5    # Find handle of the main window
     6    #
     7    print "Looking for main window's handle\n";
     8    my @hwnds = FindWindowLike( undef, "^about:blank" );
     9    if( !@hwnds ){
    10        die "Cannot find window with title/caption about:blank\n";
    11    }else{
    12        printf( "Window handle of IE application is %x\n", $hwnds[ 0 ] );
    13    }
    14    
    15    #
    16    # Find handle of the "Address" edit box
    17    #
    18    print "Looking for Address box window handle\n";
    19    @hwnds = FindWindowLike( $hwnds[ 0 ], undef, "Edit", 0xA205 );
    20    if( !@hwnds ){
    21        die "Cannot find Address edit box\n";
    22    }else{
    23        print "FindWindowLike returned " . scalar( @hwnds ). " handles\n";
    24        printf( "Window handle of Address edit box is %x\n", $hwnds[ 0 ] );
    25    }
    26    
    27    SetForegroundWindow( $hwnds[ 0 ] );
    28    SendKeys( "www.piotrkaluski.com~" );
test1.pl


In the line 8, we find a handle of main window of IE. Note that it is looking for an application, which caption begins with "about:blank" screen. In order to force IE to have such a string in its title bar, you have to type "about:blank" in its address field.
Having the window's handle, we look (in line 19) for all Edit controls, which are direct or indirect children of IE main window. Once we find it, we set its value to the new url followed by <ENTER>.
The code is not that complex. However, there are cases, were we need more sophisticated searches in the windows tree. In applications with more complex structure it is possible to have controls with the same id and of the same class on the same level. Especially, controls of new types tend to have control id equal to zero. This is the case for IE. Look below:

Control IDs
Control ids in IE

On the 4th level you have 3 ToolbarWindow32 controls. 1 with control id A000, and 2 with control id 0. Let's imagine we would like to manipulate a menu bar, which in IE is implemented by ToolbarWindow32 (on the picture above it is the last Toolbar, with handle 1102AA). In order to manipulate it, we need to know its window handle. In our example we will not actually manipulate it, we will simply find its window handle. We try to get Menu Bar's handle in the code below

     1    use Win32::GuiTest qw( FindWindowLike );
     2    use strict;
     3   
     4    #
     5    # Find a handle of the main window
     6    #
     7    print "Looking for main window's handle\n";
     8    my @hwnds = FindWindowLike( undef, "^about:blank" );
     9    if( !@hwnds ){
    10        die "Cannot find window with title/caption about:blank\n";
    11    }else{
    12        printf( "Window handle of IE application is %x\n", $hwnds[ 0 ] );
    13    }
    14   
    15    #
    16    # Find handle of the Menu Bar
    17    #
    18    @hwnds = FindWindowLike( $hwnds[ 0 ], undef, "ToolbarWindow32", 0 );
    19    if( !@hwnds ){
    20        die "Cannot find toolbars\n";
    21    }else{
    22        printf( "Found " . scalar( @hwnds ) . " handles for ToolbarWindow32 " .
    23                "with control id = 0\n" );
    24    }
    25     
test2.pl

If you run it, you will see that FindWindowLike in line 18 returns... 3 windows handles (or 4 if you are using FindWindowLike without a fix I was talking about in notes)! Why? There are 2 reasons. There is a 4th ToolbarWindow32 on the nested level (on the picture above it has a handle 907AC). We would have to do some additional processing to get the right handle. What is this processing?
First of all, FindWindowLike has a last argument, $maxlevel, which is not documented (probably by mistake). This argument specifies how deep we should look for the child window. If we set $maxlevel to 4, which means 3 levels deep (as for me this is counter intuitive and should be changed/fixed), we will get only controls, which are 3rd level descendants of the main window.
After replacing line 18 with this (you can find it in test2a.pl script):

@hwnds = FindWindowLike( $hwnds[ 0 ], undef, "ToolbarWindow32", 0, 4 );

we will get handles of 2 controls. Which one is the menu bar? Well, I will tell you what do I do in such cases. I am not sure, that my way is 100% theoretically correct, but it works for me. If someone can confirm that my assumptions are correct or wrong, please let me know. I have observed the following - if we launch a GUI application again and again (without modifying it), on each level controls will always appear in the same order. So the menu bar Toolbar will always appear as the last ToolbarWindow32 control of all children of ReBarWindow32 control. And FindWindowLike retains this order. So @hwnds contains 2 handles, and the last one (2nd) is a handle we are looking for.
So the code finding the handle of a menu bar would look like this:

     1    use Win32::GuiTest qw( FindWindowLike );
     2    use strict;
     3   
     4    #
     5    # Find a handle of the main window
     6    #
     7    print "Looking for main window's handle\n";
     8    my @hwnds = FindWindowLike( undef, "^about:blank" );
     9    if( !@hwnds ){
    10        die "Cannot find window with title/caption about:blank\n";
    11    }else{
    12        printf( "Window handle of IE application is %x\n", $hwnds[ 0 ] );
    13    }
    14   
    15    #
    16    # Find handle of the Menu Bar
    17    #
    18    @hwnds = FindWindowLike( $hwnds[ 0 ], undef, "ToolbarWindow32", 0, 4 );
    19    if( !@hwnds ){
    20        die "Cannot find toolbars\n";
    21    }else{
    22        printf( "Found " . scalar( @hwnds ) . " handles for ToolbarWindow32 " .
    23                "with control id = 0\n" );
    24        printf( "Menu bar has a window handle %x\n", $hwnds[ 1 ] );
    25    }
    26   

test3.pl


It's quite a lot of code, if you consider that it's for one control. What if we want to do something with 5 controls, and each of those controls is quite deep in the windows' hierarchy tree.

LGuiTest to the rescue

Now, let's do it with LGUITest (I will refer to it as LGT). This is the code using LGT for doing the same thing.

     1    use Win32::LGT::Window;
     2    use strict;
     3   
     4    my $win = Win32::LGT::Window->AttachMainWindow(
     5                                    'caption' => "^about:blank - Microsoft",
     6                                    'win_def_fname' => "ie.xml" );
     7   
     8    my $handle = $win->Ctrl( "MenuBar" )->{ 'handle' };
     9    printf( "Handle of Menu Bar is %x", $handle );
    10    print "\n";

test4.pl


Wow! That's cool! Is it possible that life can be so simple? Well... no. It is actually not that simple. How does LGT know, that "MenuBar" is the control we are looking for? We have to let it know, how do we want to name controls. And this is how we do it.
We have to create an xml file in which we define the windows hierarchy. I call it window definitions file. For Internet Explorer the file would look like this:

     1    <opt id="0x2002d5" caption="about:blank - Microsoft Internet Explorer" class="IEFrame">
     2        <CONTROL name="UpperPanel" caption="" class="WorkerW" id="0xa005">
     3            <CONTROL name="ReBar" caption="" class="ReBarWindow32" id="0xa005">
     4                <CONTROL id="0x0" caption="Links" class="ToolbarWindow32" idx="0">
     5                </CONTROL>
     6                <CONTROL name="MiddlePanel" caption="" class="ComboBoxEx32" id="0xa205">
     7                    <CONTROL name="Ctrl1" caption="" class="ToolbarWindow32" id="0x0">
     8                    </CONTROL>
     9                    <CONTROL name="Address" caption="" class="ComboBox" id="0xa205">
    10                        <CONTROL name="Ctrl1" caption="" class="Edit" id="0xa205">
    11                        </CONTROL>
    12                    </CONTROL>
    13                </CONTROL>
    14                <CONTROL name="Ctrl2" caption="" class="ToolbarWindow32" id="0xa000">
    15                </CONTROL>
    16                <CONTROL name="Ctrl3" caption="" class="WorkerW" id="0x0">
    17                </CONTROL>
    18                <CONTROL name="MenuBar" caption="" class="ToolbarWindow32" id="0x0" idx="1">
    19                </CONTROL>
    20            </CONTROL>
    21        </CONTROL>
    22        <CONTROL name="Ctrl2" caption="" class="WorkerW" id="0x270f">
    23        </CONTROL>
    24        <CONTROL name="Ctrl3" caption="" class="msctls_statusbar32" id="0xa001">
    25            <CONTROL name="Ctrl1" caption="" class="msctls_progress32" id="0x1">
    26            </CONTROL>
    27        </CONTROL>
    28        <CONTROL name="Ctrl4" caption="" class="Shell DocObject View" id="0x0">
    29            <CONTROL name="Ctrl1" caption="" class="Internet Explorer_Server" id="0x0">
    30            </CONTROL>
    31        </CONTROL>
    32    </opt>
ie.xml

If we run our code with the "ie.xml" file, it will fail, because the file is not complete (you will get "No such control MenuBar message"). But let's analyze it anyway. As you can see it reflects the windows' hierarchy. You can view "ie.xml" using IE or Mozilla, to see the hierarchy better. Each "CONTROL" tag represents control/sub window. Each CONTROL has a "name" or a "caption" attribute. If it has both, a name and a caption, then a name is taken into account. In order to refer to a particular control you have to specify the full path to it, i.e. all its ascendants' names separated by dots. The "MenuBar" control is a child of ReBar, which is a child of UpperPanel. So the path to "MenuBar" control is UpperPanel.ReBar.MenuBar. Let's try:

     1    use Win32::LGT::Window;
     2    use strict;
     3   
     4    my $win = Win32::LGT::Window->AttachMainWindow(
     5                                    'caption' => "^about:blank - Microsoft",
     6                                    'win_def_fname' => "ie.xml" );
     7   
     8    $win->Ctrl( "UpperPanel.ReBar.MiddlePanel.Address" )->
     9                                            SetValue( "www.piotrkaluski.com~" );
    10    my $handle = $win->Ctrl( "UpperPanel.ReBar.MenuBar" )->{ 'handle' };
    11    printf( "Handle of Menu Bar is %x", $handle );
    12    print "\n";

test5.pl

As you have probably noticed I have also included setting a value of "Address" field. Running test5.pl will insert "www.piotrkaluski.com" in address window and will print window handle of Mane Bar.
OK. But this is not exactly what I have promised in test4.pl, i.e. referring to Menu bar by simply typing "MenuBar". In order to refer to it as "MenuBar" you have to define an alias in the window definition file "ie.xml" file. Like below (bolded, underlined):

     1    <opt id="0x2002d5" caption="about:blank - Microsoft Internet Explorer" class="IEFrame">
     2        <ALIAS name="Address">UpperPanel.ReBar.MiddlePanel.Address</ALIAS>
     3        <ALIAS name="MenuBar">UpperPanel.ReBar.MenuBar</ALIAS>
     4        <CONTROL name="UpperPanel" caption="" class="WorkerW" id="0xa005">
     5            <CONTROL name="ReBar" caption="" class="ReBarWindow32" id="0xa005">
     6                <CONTROL id="0x0" caption="Links" class="ToolbarWindow32" idx="0">
     7                </CONTROL>
     8                <CONTROL name="MiddlePanel" caption="" class="ComboBoxEx32" id="0xa205">
     9                    <CONTROL name="Ctrl1" caption="" class="ToolbarWindow32" id="0x0">
    10                    </CONTROL>
    11                    <CONTROL name="Address" caption="" class="ComboBox" id="0xa205">
    12                        <CONTROL name="Ctrl1" caption="" class="Edit" id="0xa205">
    13                        </CONTROL>
    14                    </CONTROL>
    15                </CONTROL>
    16                <CONTROL name="Ctrl2" caption="" class="ToolbarWindow32" id="0xa000">
    17                </CONTROL>
    18                <CONTROL name="Ctrl3" caption="" class="WorkerW" id="0x0">
    19                </CONTROL>
    20                <CONTROL name="MenuBar" caption="" class="ToolbarWindow32" id="0x0" idx="1">
    21                </CONTROL>
    22            </CONTROL>
    23        </CONTROL>
    24        <CONTROL name="Ctrl2" caption="" class="WorkerW" id="0x270f">
    25        </CONTROL>
    26        <CONTROL name="Ctrl3" caption="" class="msctls_statusbar32" id="0xa001">
    27            <CONTROL name="Ctrl1" caption="" class="msctls_progress32" id="0x1">
    28            </CONTROL>
    29        </CONTROL>
    30        <CONTROL name="Ctrl4" caption="" class="Shell DocObject View" id="0x0">
    31            <CONTROL name="Ctrl1" caption="" class="Internet Explorer_Server" id="0x0">
    32            </CONTROL>
    33        </CONTROL>
    34    </opt>
ie_aliased.xml

Alias has a name and a path to a control it points to. Alias is created on a certain level. If it is created on a level of the main window, you will refer to address control by typing "Address".

So the last script looks like this:

     1    use Win32::LGT::Window;
     2    use strict;
     3   
     4    my $win = Win32::LGT::Window->AttachMainWindow(
     5                                    'caption' => "^about:blank - Microsoft",
     6                                    'win_def_fname' => "ie_aliased.xml" );
     7   
     8    $win->Ctrl( "Address" )->SetValue( "www.piotrkaluski.com~" );
     9    my $handle = $win->Ctrl( "MenuBar" )->{ 'handle' };
    10    printf( "Handle of Menu Bar is %x", $handle );

test6.pl

Congratulations! You're done with you first use of LGuiTest.

Summary

Whenever I hear about a new tool, my first questions are:
  1. What does the tool actually do
  2. When should I use it, and when should I use something else.
  3. What makes it better/different from other tools.

I believe I have answered the first question.
Let me answer the second and third one. So, which is better - Win32::GuiTest or Win32::LGT? From the technical point of view none is better because LGT uses Win32::GuiTest intensively. To some extent, relationship between GuiTest and LGT is like relationship between MFC and Win32 API. LGT provides an object oriented framework around GuiTest. You should use GuiTest for simple automation. You will be forced to use GuiTest if you need to do some really specific actions on some windows. If you are creating a set of more complicated test scripts, which you would like to maintain in the future - then you should consider using some kind of framework, featuring manageability and maintainability. LGT have a chance to be such a tool.
LGT features writing simpler code of test scripts.If in your code you will refer only to carefully chosen aliases, then if the tested application change, you just have to refresh window definition files and redefine aliases.
However, simplicity of the code comes at a price. You have to create the xml files with windows' hierarchy. For complex GUIs it may take some time. The good news is that you don't have to include all existing controls in window definition file. You only have to put controls you are about to use and their ancestors. For our example the following window definition file would be sufficient

     1    <opt id="0x2002d5" caption="about:blank - Microsoft Internet Explorer" class="IEFrame">
     2        <ALIAS name="Address">UpperPanel.ReBar.MiddlePanel.Address</ALIAS>
     3        <ALIAS name="MenuBar">UpperPanel.ReBar.MenuBar</ALIAS>
     4        <CONTROL name="UpperPanel" caption="" class="WorkerW" id="0xa005">
     5            <CONTROL name="ReBar" caption="" class="ReBarWindow32" id="0xa005">
     6                <CONTROL name="MiddlePanel" caption="" class="ComboBoxEx32" id="0xa205">
     7                    <CONTROL name="Address" caption="" class="ComboBox" id="0xa205">
     8                        <CONTROL name="Ctrl1" caption="" class="Edit" id="0xa205">
     9                        </CONTROL>
    10                    </CONTROL>
    11                </CONTROL>
    12                <CONTROL name="MenuBar" caption="" class="ToolbarWindow32" id="0x0" idx="1">
    13                </CONTROL>
    14            </CONTROL>
    15        </CONTROL>
    16    </opt>


I have a simple tool for generating xml for a given window. I will make the tool available as soon as it is usable for others. But even with this tool, it takes some time to make an xml file fully reflect your needs. Therefore you should use LGT when you have to run more complex scenarios on complex GUIs. Also the windows' hierarchy of a tested application should not change too often. But this constraint apply to any GUI or even more general - test automation. You should automate tests of features, which rarely change. Otherwise you will spend more time updating testing scripts then you actually save using automation.
If you obey this rule of thumb, then the initial investment may soon be returned.
 

If you are desperate about using LGuiTest...

You can read in notes how to get LGuiTest. As I said, this a young module. There are so many TODOs that I am going to create a separate article about it. However, I believe that this module has a potential to be a real help for well thought test automation. LGT has some important features, which I didn't mention here because this document is intended to be an introduction. If you want to know more, then check the project's site lguitest.sourceforge.net (there is nothing there but I am working on it) or my personal site . I will also try to start releasing LGT on regular basis using Source Forge's file releases system.

Comments

Should you have any comments on this document or you have found mistakes (including typos - they are not really important from meritorical point of view, but they annoy reader and make a bad impression about the writer), send me an e-mail : pkaluski@piotrkaluski.com

Help needed

I need a simple GUI application, which could be used as an example in this tutorial. If you are an experienced Windows developer and you want to help, drop me an e-mail.