Tải bản đầy đủ (.pdf) (32 trang)

Windows-Based UI Testing

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (345.34 KB, 32 trang )

Windows-Based UI Testing
3.0 Introduction
This chapter describes how to test an application through its user interface (UI) using low-
level Windows-based automation. These techniques involve calling Win32 API functions such
as FindWindow() and sending Windows messages such as WM_LBUTTONUP to the application
under test (AUT). Although these techniques have been available to developers and testers for
many years, the .NET programming environment dramatically simplifies the process. Figure
3-1 demonstrates the kind of lightweight test automation you can quickly create.
Figure 3-1. Windows-based UI test run
65
CHAPTER 3
■ ■ ■
6633c03.qxd 4/3/06 1:58 PM Page 65
The dummy AUT is a color-mixer application. The key code for the application is
void button1_Click(object sender, System.EventArgs e)
{
string tb = textBox1.Text;
string cb = comboBox1.Text;
if (tb == "<enter color>" || cb == "<pick>")
MessageBox.Show("You need 2 colors", "Error");
else
{
if (tb == cb)
listBox1.Items.Add("Result is " + tb);
else if (tb == "red" && cb == "blue" ||
tb == "blue" && cb =="red")
listBox1.Items.Add("Result is purple");
else
listBox1.Items.Add("Result is black");
}
}


Notice that the application may generate an error message box. Dealing with low-level
constructs such as message boxes and the main menu are tasks that can be handled well by
Win32 functions. The fundamental idea is that every Windows-based control is a window.
Each control/window has a handle that can be used to access, manipulate, and examine the
control/window. The three key categories of tasks in lightweight, low-level Windows-based UI
automation are
• Finding a window/control handle
• Manipulating a window/control
• Examining a window/control
Keeping this task-organization structure in mind will help you arrange your test automation.
The code in this chapter is written in a traditional procedural style rather than in an object-
oriented style. This is a matter of personal preference, and you may want to recast the techniques
to an OOP (object-oriented programming) style. Additionally, you may want to modularize the
code solutions further by combining them into a .NET class library. The test automation harness
that produced the test run shown in Figure 3-1 is presented in Section 3.10.
3.1 Launching the AUT
Problem
You want to launch a Windows form-based application so you can test it through its UI.
Design
Use the System.Diagnostics.Process.Start() method.
CHAPTER 3

WINDOWS-BASED UI TESTING66
6633c03.qxd 4/3/06 1:58 PM Page 66
Solution
static void Main(string[] args)
{
try
{
Console.WriteLine("\nLaunching application under test");

string path = "..\\..\\..\\WinApp\\bin\\Debug\\WinApp.exe";
Process p = Process.Start(path);
if (p == null)
Console.WriteLine("Warning: process may already exist");
// run UI test scenario here
Console.WriteLine("\nDone");
}
catch(Exception ex)
{
Console.WriteLine("Fatal error: " + ex.Message);
}
}
There are several ways to launch a Windows form application so that you can test it through
its UI using Windows-based techniques. The simplest way is to use the Process.Start() static
method located in the System.Diagnostics namespace.
Comments
The Process.Start() method has four overloads. The overload used in this solution accepts a
path to the AUT and returns a Process object that represents the resources associated with the
application. You need to be a bit careful with the Process.Start() return value. A return of null
does not necessarily indicate failure; null is also returned if an existing process is reused. Regard-
less, a return of null is not good because your UI test automation will often become confused if
more than one target application is running. This idea is explained more fully in Section 3.2.
If you need to pass arguments to the AUT, you can use the Process.Start() overload that
accepts a second argument, which represents command-line arguments to the application.
For example:
Process p = null;
p = Process.Start("SomeApp.exe", "C:\\Somewhere\\Somefile.txt");
if (p == null)
Console.WriteLine("Warning: process may already exist");
The Process.Start() method also supports an overload that accepts a ProcessStartInfo

object as an argument. A ProcessStartInfo object can direct the AUT to launch and run in a
variety of ways; however, this technique is rarely needed in a lightweight test automation sce-
nario. The Process.Start() method is asynchronous, so when you use it to launch the AUT, be
careful about attempting to access the application through your test harness until after you
are sure the application has launched. This problem is discussed and solved in Section 3.2.
CHAPTER 3

WINDOWS-BASED UI TESTING 67
6633c03.qxd 4/3/06 1:58 PM Page 67
3.2 Obtaining a Handle to the Main Window of the AUT
Problem
You want to obtain a handle to the application main window.
Design
Use the FindWindow() Win32 API function with the .NET platform invoke (P/Invoke) mechanism.
Solution
class Class1
{
[DllImport("user32.dll", EntryPoint="FindWindow",
CharSet=CharSet.Auto)]
static extern IntPtr FindWindow(string lpClassName,
string lpWindowName);
[STAThread]
static void Main(string[] args)
{
try
{
// launch AUT; see Section 3.1
IntPtr mwh = IntPtr.Zero; // main window handle
bool formFound = false;
int attempts = 0;

while (!formFound && attempts < 25)
{
if (mwh == IntPtr.Zero)
{
Console.WriteLine("Form not yet found");
Thread.Sleep(100);
++attempts;
mwh = FindWindow(null, "Form1");
}
else
{
Console.WriteLine("Form has been found");
formFound = true;
}
}
if (mwh == IntPtr.Zero)
throw new Exception("Could not find main window");
CHAPTER 3

WINDOWS-BASED UI TESTING68
6633c03.qxd 4/3/06 1:58 PM Page 68
Console.WriteLine("\nDone");
}
catch(Exception ex)
{
Console.WriteLine("Fatal error: " + ex.Message);
}
}
} // Class1
To manipulate and examine the state of a Windows application, you must obtain a handle

to the application’s main window. A window handle is a system-generated value that you can
think of as being both an ID for the associated window and a way to access the window.
Comments
In a .NET environment, a window handle is type System.IntPtr, which is a platform-specific
type used to represent either a pointer (memory address) or a handle. To obtain a handle to
the main window of an AUT, you can call the Win32 API FindWindow() function. The
FindWindow() function is essentially a part of the Windows operating system, which is
available to you. Because FindWindow() is part of Windows, it is written in traditional C++
and not managed code. The C++ signature for FindWindow() is
HWND FindWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName);
This function accepts a window class name and a window name as arguments, and it
returns a handle to the window. To call into unmanaged code like the FindWindow() function
from C#, you can use a .NET mechanism called platform invoke (P/Invoke). P/Invoke func-
tionality is contained in the System.Runtime.InteropServices namespace. The mechanism is
very elegant. In essence, you create a C# wrapper, or alias for the Win32 function you want to
use, and then call that alias. You start by placing a
using System.Runtime.InteropServices;
statement in your test harness so you can easily access P/Invoke functionality. Next you
determine a C# signature for the unmanaged function you want to call. This really involves
deter- mining C# data types that map to the return type and parameter types of the unman-
aged function. In the case of FindWindow(), the unmanaged return type is HWND, which is a
Win32 data type representing a handle to a window. As explained earlier, the corresponding
C# data type is System.IntPtr. The Win32 FindWindow() function accepts two parameters of
type LPCTSTR. Although the details are fairly deep, this is basically a Win32 data type that can
be represented by a C# type string.

Note
One of the greatest productivity-enhancing improvements that .NET introduced to application develop-
ment is a vastly simplified data type model. To use the P/Invoke mechanism, you must determine the C# equiv-
alents to Win32 data types. A detailed discussion of the mappings between Win32 data types and .NET data

types is outside the scope of this book, but fortunately most mappings are fairly obvious. For example, the Win32
data types
LPCSTR
,
LPCTSTR
,
LPCWSTR
,
LPSTR
,
LPTSTR
, and
LPWSTR
usually map to the C# string data type.
CHAPTER 3

WINDOWS-BASED UI TESTING 69
6633c03.qxd 4/3/06 1:58 PM Page 69
After determining the C# alias method signature, you can place a class-scope DllImport
attribute with the C# method signature that corresponds to the Win32 function signature into
your test harness:
[DllImport("user32.dll", EntryPoint="FindWindow",
CharSet=CharSet.Auto)]
static extern IntPtr FindWindow(string lpClassName,
string lpWindowName);
The “user32.dll” argument specifies the DLL file where the unmanaged function you
want to use is located. Because the DllImport attribute is expecting a DLL, the .dll extension
is optional; however, including it makes your code more readable. The EntryPoint attribute
specifies the name of the Win32 API function that you will be calling through the C# alias. If
the C# method name is exactly the same as the Win32 function name, you may omit the

EntryPoint argument. But again, putting the argument in the attribute makes your code easier
to read and maintain. The CharSet argument is optional but should be used whenever the C#
method alias has a return type or one or more parameters that are type char or string. Speci-
fying CharSet.Auto essentially means to let the .NET Framework take care of all character type
conversions, for example, ASCII to Unicode. The CharSet.Auto argument dramatically simpli-
fies working with type char and type string.
When you code the C# method alias for a Win32 function, you almost always use the
static and extern modifiers because most Win32 functions are static functions rather than
instance functions in C# terminology, and Win32 functions are external to your test harness.
You may name the C# method anything you like but keeping the C# method name the same
as the Win32 function name is the most readable approach. Similarly, you can name the C#
parameters anything you like, but again, a good strategy is to make C# parameter names the
same as their Win32 counterparts.
With the P/Invoke plumbing in place, if a subtle timing issue did not exist, you could now
get the handle to the main window of the AUT like this:
IntPtr mwh = FindWindow(null, "Form1");
Before explaining the timing issue, let’s look at the method call. The second argument to
FindWindow() is the window name. In help documentation, this value is sometimes called the
window title or the window caption. In the case of a Windows form application, this will usually
be the form name. The first argument to FindWindow() is the window class name. A window
class name is a system-generated string that is used to register the window with the operating
system. Note that the term “class name” in this context is an old pre-OOP term and is not at all
related to the idea of a C# language class container structure. Window/control class names are
not unique, so they have little value when trying to find a window/control.
In this example, if you pass null as the window class name when calling FindWindow(),
FindWindow() will return the handle of the first instance of a window with name "Form1". This
means you should be very careful about having multiple AUTs active, because you may get the
wrong window handle.
If you attempt to obtain the application main window handle in the simple way just
described, you are likely to run into a timing issue. The problem is that your AUT may not be

fully launched and registered. A poor way to deal with this problem is to place Thread.Sleep()
calls with large delays into your test harness to give the application time to launch. A better
CHAPTER 3

WINDOWS-BASED UI TESTING70
6633c03.qxd 4/3/06 1:58 PM Page 70
way to deal with this issue is to wrap the call to FindWindow() in a while loop with a small
delay, checking to see if you get a valid window handle:
IntPtr mwh = IntPtr.Zero; // main window handle
bool formFound = false;
while (!formFound)
{
if (mwh == IntPtr.Zero)
{
Console.WriteLine("Form not yet found");
Thread.Sleep(100);
mwh = FindWindow(null, "Form1");
}
else
{
Console.WriteLine("Form has been found");
formFound = true;
}
}
You use a Boolean flag to control the while loop. If the value of the main window handle is
IntPtr.Zero, then you delay the test automation by 100 milliseconds (one-tenth of a second)
using the Thread.Sleep() method from the System.Threading namespace. This approach could
lead to an infinite loop if the main window handle is never found, so in practice you will often
want to add a counter to limit the maximum number of times you iterate through the loop:
IntPtr mwh = IntPtr.Zero; // main window handle

bool formFound = false;
int attempts = 0;
while (!formFound && attempts < 25)
{
if (mwh == IntPtr.Zero)
{
Console.WriteLine("Form not yet found");
Thread.Sleep(100);
++attempts;
mwh = FindWindow(null, "Form1");
}
else
{
Console.WriteLine("Form has been found");
formFound = true;
}
}
if (mwh == IntPtr.Zero)
throw new Exception("Could not find Main Window");
CHAPTER 3

WINDOWS-BASED UI TESTING 71
6633c03.qxd 4/3/06 1:58 PM Page 71
If the value of the main window handle variable is still IntPtr.Zero after the while loop
terminates, you know that the handle was never found, and you should abort the test run by
throwing an exception.
You can increase the modularity of your lightweight test harness by wrapping the code in
this solution in a helper method. For example, if you write
static IntPtr FindMainWindowHandle(string caption)
{

IntPtr mwh = IntPtr.Zero;
bool formFound = false;
int attempts = 0;
do
{
mwh = FindWindow(null, caption);
if (mwh == IntPtr.Zero)
{
Console.WriteLine("Form not yet found");
Thread.Sleep(100);
++attempts;
}
else
{
Console.WriteLine("Form has been found");
formFound = true;
}
} while (!formFound && attempts < 25);
if (mwh != IntPtr.Zero)
return mwh;
else
throw new Exception("Could not find Main Window");
} // FindMainWindowHandle()
then you can make a clean call in your harness Main() method like this:
Console.WriteLine("Finding main window handle");
IntPtr mwh = FindMainWindowHandle("Form1");
Console.WriteLine("Handle to main window is " + mwh);
Depending on the complexity of your AUT, you may want to parameterize the delay time
and the maximum number of attempts, leading to a helper signature such as
static IntPtr FindMainWindowHandle(string caption, int delay,

int maxTries)
which can be called like this:
CHAPTER 3

WINDOWS-BASED UI TESTING72
6633c03.qxd 4/3/06 1:58 PM Page 72
Console.WriteLine("Finding main window handle");
int delay = 100;
int maxTries = 25;
IntPtr mwh = FindMainWindowHandle("Form1", delay, maxTries);
Console.WriteLine("Handle to main window is " + mwh);
3.3 Obtaining a Handle to a Named Control
Problem
You want to obtain a handle to a control/window that has a window name.
Design
Use the FindWindowEx() Win32 API function with the .NET P/Invoke mechanism.
Solution
IntPtr mwh = IntPtr.Zero; // main window handle
// obtain main window handle here; see Section 3.2
Console.WriteLine("Finding handle to textBox1");
IntPtr tb = FindWindowEx(mwh, IntPtr.Zero, null, "<enter color>");
if (tb == IntPtr.Zero)
throw new Exception("Unable to find textBox1");
else
Console.WriteLine("Handle to textBox1 is " + tb);
Console.WriteLine("Finding handle to button1");
IntPtr butt = FindWindowEx(mwh, IntPtr.Zero, null, "button1");
if (butt == IntPtr.Zero)
throw new Exception("Unable to find button1");
else

Console.WriteLine("Handle to button1 is " + butt);
where a class-scope DllImport attribute is
[DllImport("user32.dll", EntryPoint="FindWindowEx",
CharSet=CharSet.Auto)]
static extern IntPtr FindWindowEx(IntPtr hwndParent,
IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
To access and manipulate a control on a form-based application, you must obtain a han-
dle to the control. In a Windows environment, all GUI controls are themselves windows. So, a
button control is a window, a textbox control is a window, and so forth. To get a handle to a
control/window, you can use the FindWindowEx() Win32 API function.
CHAPTER 3

WINDOWS-BASED UI TESTING 73
6633c03.qxd 4/3/06 1:58 PM Page 73
Comments
To call a Win32 function such as FindWindowEx() from a C# test harness, you can use the
P/Invoke mechanism as described in Section 3.2. The Win32 FindWindowEx() function has
this C++ signature:
HWND FindWindowEx(HWND hwndParent, HWND hwndChildAfter,
LPCTSTR lpszClass, LPCTSTR lpszWindow);
The FindWindowEx() function accepts four arguments. The first argument is a handle to the
parent window of the control you are seeking. The second argument is a handle to a control
and directs FindWindowEx() where to begin searching; the search begins with the next child
control. The third argument is the class name of the target control, and the fourth argument is
the window name/title/caption of the target control.
As discussed in Section 3.2, the C# equivalent to the Win32 type HWND is IntPtr and the C#
equivalent to type LPCTSTR is string. Because the Win32 FindWindowEx() function is located in
file user32.dll, you can insert this class-scope attribute and C# alias into the test harness:
[DllImport("user32.dll", EntryPoint="FindWindowEx",
CharSet=CharSet.Auto)]

static extern IntPtr FindWindowEx(IntPtr hwndParent,
IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
Notice that the C# alias method signature uses the same function name and same param-
eter names as the Win32 function for code readability. With this P/Invoke plumbing in place,
you can obtain a handle to a named control:
// get main window handle in variable mwh; see Section 3.2
Console.WriteLine("Finding handle to textBox1");
IntPtr tb = FindWindowEx(mwh, IntPtr.Zero, null, "<enter color>");
Console.WriteLine("Finding handle to button1");
IntPtr butt = FindWindowEx(mwh, IntPtr.Zero, null, "button1");
The first argument is the handle to the main window form that contains the target control.
By specifying IntPtr.Zero as the second argument, you instruct FindWindowEx() to search all
controls on the main form window. You ignore the target control class name by passing in null
as the third argument. The fourth argument is the target control’s name/title/caption.
You should not assume that a call to FindWindowEx() has succeeded. To check, you can
test if the return handle has value IntPtr.Zero along the lines of
if (tb == IntPtr.Zero)
throw new Exception("Unable to find textBox1");
if (butt == IntPtr.Zero)
throw new Exception("Unable to find button1");
So, just how do you determine a control name/title/caption? The simplest way is to use
the Spy++ tool included with Visual Studio .NET. The Spy++ tool is indispensable for light-
weight UI test automation. Figure 3-2 shows Spy++ after its window finder has been placed on
the button1 control of the AUT shown in the foreground of Figure 3-1.
CHAPTER 3

WINDOWS-BASED UI TESTING74
6633c03.qxd 4/3/06 1:58 PM Page 74
Figure 3-2. The Spy++ tool
In addition to a control’s caption, Spy++ provides other useful information such as the

control’s class name, Windows events related to the control, and the control’s parent, child,
and sibling controls.
3.4 Obtaining a Handle to a Non-Named Control
Problem
You want to obtain a handle to a control that does not have a window name.
Design
Write a FindWindowByIndex() helper method that finds the control by using its implied index
value.
CHAPTER 3

WINDOWS-BASED UI TESTING 75
6633c03.qxd 4/3/06 1:58 PM Page 75
Solution
static IntPtr FindWindowByIndex(IntPtr hwndParent, int index)
{
if (index == 0)
return hwndParent;
else
{
int ct = 0;
IntPtr result = IntPtr.Zero;
do
{
result = FindWindowEx(hwndParent, result, null, null);
if (result != IntPtr.Zero)
++ct;
} while (ct < index && result != IntPtr.Zero);
return result;
}
}

and then call like this:
Console.WriteLine("Finding handle to listBox1");
IntPtr lb = FindWindowByIndex(mwh, 3);
if (lb == IntPtr.Zero)
throw new Exception("Unable to find listBox1");
else
Console.WriteLine("Handle to listBox1 is " + lb);
To access and manipulate a control on a form-based application, you must obtain a han-
dle to the control. If the target control has a unique window name, then you can obtain its
handle using the techniques in Section 3.3. But a control may not have a name/caption/title.
Examples include empty textbox controls and empty listbox controls. Furthermore, controls
may have nonunique names. To deal with these situations, you can write a helper method that
uses the Win32 FindWindowEx() function to return a control handle based on the control’s
order index value.
Comments
The index value of a control is implied rather than explicit. The idea is that each control on a
form has a predecessor and a successor control (except for the first control, which has no
predecessor, and the last control, which has no successor). This predecessor-successor rela-
tionship can be used to find window handles.
Before examining this control index order concept further, let’s imagine that we know the
index value of a control and see how the FindWindowByIndex() helper method works to return
the control handle. Suppose, for example, that an application has a listbox control, and the
index of the control is 3. This means that index 0 represents the main form window, and indexes
1 and 2 represent predecessor controls to the listbox control. The FindWindowByIndex() helper
CHAPTER 3

WINDOWS-BASED UI TESTING76
6633c03.qxd 4/3/06 1:58 PM Page 76

Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Tải bản đầy đủ ngay
×