March 2009

Volume 24 Number 03

Test Run - Automating UI Tests In WPF Applications

By James McCaffrey | March 2009

Code download available

Contents

The WPF Application Under Test
The UI Test Automation
Final Words

In this month's column, I show you how to write UI test automation for Windows Presentation Foundation (WPF) applications. WPF applications use a new graphics subsystem, and most traditional UI test automation techniques simply don't work with WPF apps. Luckily, the Microsoft UI Automation (MUIA) library was designed with WPF application UI automation in mind. You can also use the Microsoft UI Automation library to test classic Win32 applications and WinForm-based .NET applications on host machines running operating systems that support the Microsoft .NET 3.0 Framework.

Compared with older alternatives to UI automation, the Microsoft UI Automation library is more powerful and more consistent, and after an initial learning curve, you'll find it is much easier to use. This column assumes you have some basic familiarity with WPF applications, intermediate level C# skills, but no experience with the MUIA library.

A good way to show you where I'm headed is with a screenshot. The image in Figure 1shows that I am testing a simple but representative WPF application. The application is called CryptoCalc, and it computes a cryptographic hash of an input string using one of three hashing techniques: MD5 hashing, SHA1 hashing, or DES encryption.

Figure 1 WPF Application UI Automation

The MD5 (Message Digest 5) hashing technique accepts an arbitrarily sized array of bytes and returns a 16-byte fingerprint that can then be used for various identification purposes. The SHA1 (Secure Hash Algorithm 1) hashing technique is similar to MD5, except that SHA1 uses a different algorithm and returns a 20-byte fingerprint. The DES (Digital Encryption Standard) is a symmetric key encryption technique that can also be used to produce an identifying byte array. DES crypto-hashing returns a byte array that is at least as large as the number of input bytes.

The UI test automation shown in Figure 1is run through a console application that launches the application under test; uses the Microsoft UI Automation library to obtain references to the application and user controls on the app; and simulates a user entering "Hello!", selecting the DES Encrypt option, and clicking on the Compute button control. The test automation then checks the resulting state of the application under test by examining the result text box control for an expected value and prints a pass or fail result. I captured the screen shot in Figure 1just before the test automation closed the application under test by simulating user clicks on the File and then Exit menu items.

In the sections of this column that follow, I will briefly describe the CryptoCalc WPF application I am testing, explain how to launch the application under test, how to use the Microsoft UI Automation library to get references to the application and user controls, how to simulate user actions, and how to check application state. I'll also describe how you can extend and modify the test system presented here to meet your own needs. I think you'll find the ability to use the Microsoft UI Automation library to test WPF applications a neat addition to your personal tool set.

The WPF Application Under Test

Let's take a brief look at the WPF application under test so that you'll understand the goal of the test automation and understand the design issues that affect UI test automation. The CryptoCalc application is a simple, single-window user application that computes a cryptographic hash of a string. The application accepts a user-supplied string of up to 24 characters in a TextBox control, converts the input string into an array of bytes, computes one of three types of crypto-hashes of the input bytes, and displays the resulting hashed bytes in a second TextBox control.

I designed the app under test using Visual Studio 2008 with C# and named the Project Crypto­Calc. The WPF template generates a skeleton application UI definition as a XAML file:

<Window x:Class="CryptoCalc.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> <Grid></Grid> </Window>

Notice that the top-level Window control definition does not contain a Name attribute. This is significant because, as we'll see shortly, when you write test automation, an easy way to get a reference to a control using the MUIA library is to access the AutomationId property, which is generated by the compiler from the control's Name attribute. Controls without a XAML Name attribute will not receive an AutomationId property. This idea is a specific, low-level example of the importance of considering application design issues for things such as security, extensibility, and test automation.

Next, I added a Label control and a TextBox control by dragging items from the Visual Studio Toolbox onto the design surface:

<Label Height="28" HorizontalAlignment="Left" Margin="10,33,0,0" Name="label1" VerticalAlignment="Top" Width="120">Enter string:</Label> <TextBox MaxLength="24" Height="23" Margin="10,57,51,0" Name="textBox1" VerticalAlignment="Top" />

Notice that by default, these controls do receive normal Name attributes—label1 and textBox1, respectively. Next, I placed three RadioButton controls inside a GroupBox control. Unlike the old WinForms GroupBox control, a WPF GroupBox accepts only a single item, so I wrapped my three RadioButton controls into a single StackPanel container (which can contain multiple items):

<GroupBox Header="Crypto Type" Margin="10,91,118,127" Name="groupBox1"> <StackPanel Height="52" Name="stackPanel1" Width="127"> <RadioButton Height="16" Name="radioButton1" Width="120"> MD5 Hash</RadioButton> <RadioButton Height="16" Name="radioButton2" Width="120"> SHA1 Hash</RadioButton> <RadioButton Height="16" Name="radioButton3" Width="120"> DES Encrypt</RadioButton> </StackPanel> </GroupBox>

Next, I placed a Button control to trigger the crypto-hash computation and a TextBox control to hold the result onto the main Window control:

<Button Height="23" Margin="10,0,0,90" Name="button1" VerticalAlignment="Bottom" Click="button1_Click" HorizontalAlignment="Left" Width="89">Compute</Button> <TextBox Height="63" Margin="10,0,51,13" Name="textBox2" VerticalAlignment="Bottom" TextWrapping="Wrap" />

One of the features I really like about WPF applications is the new Menu control paradigm, which, unlike WinForm menu items, treats menus and submenus as ordinary user controls. First, I dragged a main Menu container control to the top part of the CryptoCalc application:

<Menu Height="22" Name="menu1" VerticalAlignment="Top" IsMainMenu="True" > </Menu>

Visual Studio 2008 does not support a drag-and-drop design for child MenuItem controls, so I added my menu items to the XAML definition file manually by adding a top-level File menu item:

<MenuItem Header="_File"> <MenuItem Header="_New" Name="fileNew" /> <MenuItem Header="_Open" Name="fileOpen" /> <Separator /> <MenuItem Header="E_xit" Name="fileExit" InputGestureText="Alt-F4" ToolTip="Exit CryptoCalc" Click="OnFileExit" /> </MenuItem>

WPF design supports the usual accelerator key syntax, such as _File. The <Separator /> tag is clean and easy. The ToolTip attribute will generate code that displays a short help message when the user mouses over the associated MenuItem control. When compiled, the Click OnFileExit attribute will generate C# code that expects an event handler with this signature:

public void OnFileExit(object sender, RoutedEventArgs e) { //... }

So, after I added the regular application logic code to the Crypto­Calc app, I manually added the event handler and then supplied functionality by placing this.Close into the method body. Notice that the New and Open menu item controls do not have any associated events at this time. I did this to point out that UI test automation often takes place during development when many application features are incomplete. The top-level Help control design follows the same pattern as the File control:

<MenuItem Header="_Help"> <MenuItem Header="Help Topics" /> <Separator /> <MenuItem Header="About CryptoCalc" /> </MenuItem>

The Menu container and MenuItem control definitions will generate C# code that in turn produces the user interface shown in the screenshot in Figure 2. With the UI design done, I switched over to code view. Then I added two using statements to the using statements generated by Visual Studio in the file Window1.xaml.cs so that I could access crypto classes and methods without fully qualifying their names:

using System.Security.Cryptography; using System.IO;

fig02.gif

Figure 2 WPF App File Menu

The main functionality of the simple CryptoCalc application is contained in the button1_Click method and is listed in Figure 3. My code first grabs the text in the textBox1 control and converts that text to a byte array. Note that to keep the size of my example code short, I omit the normal error-checking you'd perform in a production environment.

Figure 3 StatCalc Application Code

private void button1_Click(object sender, RoutedEventArgs e) { string input = textBox1.Text; byte[] inputBytes = Encoding.UTF8.GetBytes(input); if (radioButton1.IsChecked == true) { MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider(); byte[] hashedBytes = md5.ComputeHash(inputBytes); textBox2.Text = BitConverter.ToString(hashedBytes); } else if (radioButton2.IsChecked == true) { SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider(); byte[] hashedBytes = sha.ComputeHash(inputBytes); textBox2.Text = BitConverter.ToString(hashedBytes); } else if (radioButton3.IsChecked == true) { DESCryptoServiceProvider des = new DESCryptoServiceProvider(); byte[] blanks = System.Text.Encoding.UTF8.GetBytes(" "); // 8 spaces des.Key = blanks; des.IV = blanks; des.Padding = PaddingMode.Zeros; MemoryStream ms = new MemoryStream(); CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(), CryptoStreamMode.Write); cs.Write(inputBytes, 0, inputBytes.Length); cs.Close(); byte[] encryptedBytes = ms.ToArray(); ms.Close(); textBox2.Text = BitConverter.ToString(encryptedBytes); } }

The application logic branches depending on which RadioButton control is selected. Interestingly, because the IsChecked property returns type ?bool (nullable Boolean), I had to explicitly check the property for equality against true. The code for MD5 and SHA1 hashing should be self-explanatory.

The DES encryption algorithm requires a 64-bit key. Because I am using DES for hashing rather than encoding and decoding, I use a dummy key generated by 8 space characters. When using symmetric key encryption for hashing purposes, I usually use a null key of { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, but the DESCryptoServiceProvider object flags this as a known weak key and throws an exception. I use the same blank-space byte array to supply a value for the so-called initialization vector.

DES encryption works on blocks of 8 bytes; therefore, I specify PaddingMode.Zeros so that any input will be padded with zeros to bring the size of the input up to an even multiple of 8 bytes. Note that you would not use PaddingMode.Zeros for encoding and decoding because padding with zeros does not allow you to decrypt (the decryption algorithm cannot determine which zeros in the cipher-text are padding and which are part of the original plain-text).

The UI Test Automation

When writing test automation using the Microsoft UI Automation library, you need to know how to identify the controls on the application under test. A good way to do this is to use the UISpy tool. For those who don't know, UISpy is the WPF equivalent of the old Spy++ tool and allows you to examine properties of the UI components of a WPF application. The UISpy tool is part of the Microsoft Windows SDK and is available as a free download from microsoft.com/downloads.

The screenshot in Figure 4shows the result of targeting the Button control on the CryptoCalc application. From a test automation perspective, the important fields for identifying application controls are the ControlType (ControlType.Edit in this case), AutomationId (button1), and Name (Compute) values. The important field for manipulating application controls is the ControlPatterns list (Invoke).

Figure 4 Examining Controls with UISpy

Manually testing even this tiny CryptoCalc application through its user interface would be boring, error-prone, time-consuming, and inefficient. You would have to enter an input, click the Compute button control, visually verify the answer, and manually record the pass/fail result. A much better approach is to use the Microsoft UI Automation library to write test automation that simulates a user exercising the application and then determines whether the application responded correctly. By automating tedious test cases, you can free up time for more interesting and useful manual test cases in which your experience and intuition play a big role.

The overall structure of the test harness that produced the output shown in Figure 1is listed in Figure 5. I launched Visual Studio 2008 and created a new console application program. I used C#, but you should be able to easily convert my test automation code to Visual Basic .NET if you want to. Next, I added Project References to the UIAutomationClient.dll and UIAutomationTypes.dll libraries. These libraries are part of the .NET Framework 3.0 but are not visible by default to a Visual Studio project. The libraries are typically located in the C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0 directory. Note that the UIAutomationClient.dll library contains the key classes needed for test automation. The UIAutomationTypes.dll library contains various type definitions used by MUIA test automation.

Figure 5 UI Test Automation Code Structure

using System; using System.Diagnostics; using System.Threading; using System.Windows.Automation; namespace Harness { class Program { static void Main(string[] args) { try { Console.WriteLine("\nBegin WPF UIAutomation test run\n"); // launch CryptoCalc application // get reference to main Window control // get references to user controls // manipulate application // check resulting state and determine pass/fail Console.WriteLine("\nEnd automation\n"); } catch (Exception ex) { Console.WriteLine("Fatal: " + ex.Message); } } // Main() } // class } // ns

For convenience, I add using statements that point to the System.Diagnostics namespace (so I can easily use the Process class), and to the System.Threading namespace (so I can easily use the Thread.Sleep() method). As usual with any test automation, I wrap my harness with a top-level try-catch block to handle any fatal errors. My test automation code begins by launching the application under test:

Console.WriteLine("Launching CryptoCalc application"); Process p = null; p = Process.Start("..\\..\\..\\CryptoCalc\\bin\\Debug\\CryptoCalc.exe");

Now, before I attempt any test automation, I need to verify that the process associated with the CryptoCalc application under test is registered on the host machine. Although I could pause my test harness by simply inserting a Thread.Sleep statement, I have no good way of knowing how long to pause. A better approach is to use a smart delay-loop:

int ct = 0; do { Console.WriteLine("Looking for CryptoCalc process. . . "); ++ct; Thread.Sleep(100); } while (p == null && ct < 50);

Here I pause for 100 milliseconds each time through the delay loop. The harness exits the delay loop if the process object becomes non-null, meaning the process has been found, or if the loop has executed 50 times. Figure 6shows how I can determine if the delay loop timed-out or the AUT process was found.

Figure 6 Determining What Happened

if (p == null) throw new Exception("Failed to find CryptoCalc process"); else Console.WriteLine("Found CryptoCalc process"); // Next I fetch a reference to the host machine's Desktop as an // AutomationElement object: Console.WriteLine("\nGetting Desktop"); AutomationElement aeDesktop = null; aeDesktop = AutomationElement.RootElement; if (aeDesktop == null) throw new Exception("Unable to get Desktop"); else Console.WriteLine("Found Desktop\n");

You can think of all the controls of the WPF application under test as children of the main application Window control. The main Window is a child of the overall Desktop, so I need a reference to the Desktop in order to get a reference to the application. Now I use the FindFirst method to attach to the application under test:

AutomationElement aeCryptoCalc = null; int numWaits = 0; do { Console.WriteLine("Looking for CryptoCalc main window. . . "); aeCryptoCalc = aeDesktop.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "CryptoCalc")); ++numWaits; Thread.Sleep(200); } while (aeCryptoCalc == null && numWaits < 50);

I use the smart delay-loop technique instead of the arbitrarily long Sleep technique. The FindFirst method is used when the control you are looking for is a single control. FindFirst accepts two arguments. The first is a scope value. The three most common scope types used in test automation are TreeScope.Children, TreeScope.Descendants, and TreeScope.Parent. Because the CryptoCalc application is a direct child of the Desktop, I use the TreeScope.Children scope.

The second argument to FindFirst is an object that represents information that identifies the control you are looking for. Here I specify that I am looking for a control that has a Name property with value "CryptoCalc". I could also have used the AutomationId property, as I'll describe shortly. Now I verify that I do indeed have a reference to the main application Window control:

if (aeCryptoCalc == null) throw new Exception("Failed to find CryptoCalc main window"); else Console.WriteLine("Found CryptoCalc main window");

Once I have the application, I can use its reference to obtain references to all the user controls that the test automation will manipulate or examine. I start by grabbing the Button control:

Console.WriteLine("\nGetting all user controls"); AutomationElement aeButton = null; aeButton = aeCryptoCalc.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.NameProperty, "Compute")); if (aeButton == null) throw new Exception("No compute button"); else Console.WriteLine("Got Compute button");

I use the same pattern that I used to get a reference to the main Window; here the Button control is a direct child of the main Window application control. Because the Button control is a static control, I do not need to use the delay-loop technique before accessing the reference to the control. In the case of dynamic controls, you should use the delay-loop technique.

Next, I want to get references to the two TextBox controls. Even though the two TextBox controls have names textBox1 and textBox2, the controls do not receive Name properties, so I cannot use the same NameProperty pattern that I used for the Button control. However, the TextBox controls do receive an AutomationId property that I could use to get a reference to the controls, such as:

aeTextBox1 = aeCryptoCalc.FindFirst(TreeScope.Children, new PropertyCondition(AutomationElement.AutomationIdProperty, "textBox1"));

Instead, I decided to use a different approach, mostly for demonstration purposes. Rather than identify a single control using the control's name property, I use the FindAll method to fetch a collection of controls by control type. It turns out that TextBox controls are ControlType.Edit types, so my code grabs all TextBox controls:

AutomationElementCollection aeAllTextBoxes = null; aeAllTextBoxes = aeCryptoCalc.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit)); if (aeAllTextBoxes == null) throw new Exception("No textboxes collection"); else Console.WriteLine("Got textboxes collection");

Once I have this collection I can access each TextBox using array indexing:

AutomationElement aeTextBox1 = null; AutomationElement aeTextBox2 = null; aeTextBox1 = aeAllTextBoxes[0]; aeTextBox2 = aeAllTextBoxes[1]; if (aeTextBox1 == null || aeTextBox2 == null) throw new Exception("TextBox1 or TextBox2 not found"); else Console.WriteLine("Got TextBox1 and TextBox2");

In general, obtaining control references by using the Name property or the AutomationId property is a better approach than using ControlType, but in some scenarios you may have no choice. Next, I get a reference to the RadioButton control I want to use in my test scenario:

AutomationElement aeRadioButton3 = null; aeRadioButton3 = aeCryptoCalc.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "DES Encrypt")); if (aeRadioButton3 == null) throw new Exception("No RadioButton"); else Console.WriteLine("Got RadioButton3");

I use a pattern similar to the one I used to get a reference to the Button control. However, notice that I specify TreeScope.Descendants rather than TreeScope.Children. I do this because the RadioButton control is a child of the GroupBox control and so is not a direct child of the main Window control. Alternatively, I could have first obtained a reference to the GroupBox control (as a child of the main Window control) and then used that reference to obtain a reference to the RadioButton control. Once I have references to my controls, I can start manipulating the application under test. I begin by simulating user input into the TextBox1 control:

Console.WriteLine("\nSetting input to 'Hello1!'"); ValuePattern vpTextBox1 = (ValuePattern)aeTextBox1.GetCurrentPattern(ValuePattern.Pattern); vpTextBox1.SetValue("Hello!");

Using a SetValue method probably does not come as a surprise, but notice that I do not access SetVaue() directly through the aeTextBox1 object. Instead, I use an intermediary ValuePattern object. The concept of AutomationPattern objects such as ValuePattern is probably the biggest conceptual hurdle for engineers new to the Microsoft UI Automation library. You can think of pattern objects as an abstraction to expose a control's functionality that is independent of the control's type or appearance. Put another way, you can use specific AutomationPattern instances such as ValuePattern to enable specific control functionality.

I simplify things further by thinking that a control's ControlType exposes what kind of control the control is and that a control's Pattern exposes what the control can do. I use a similar approach to simulating a user selecting the RadioButton3 control:

Console.WriteLine("Selecting 'DES Encrypt' "); SelectionItemPattern spSelectRadioButton3 = (SelectionItemPattern)aeRadioButton3.GetCurrentPattern( SelectionItemPattern.Pattern); spSelectRadioButton3.Select();

This time I use the SelectionItemPattern to enable a selection. The name of the GetCurrentPattern method sometimes confuses MUIA library beginners. From a test automation point of view, the method is setting, not getting, a specified AutomationPattern. But from a client-server perspective, the automation client code is fetching a particular property from the application under test server code.

The code I use to simulate a click on the Calculate button control should help clarify:

Console.WriteLine("\nClicking on Compute button"); InvokePattern ipClickButton1 = (InvokePattern)aeButton.GetCurrentPattern( InvokePattern.Pattern); ipClickButton1.Invoke(); Thread.Sleep(1500);

Here, in essence, I use the InvokePattern to enable a button click and then execute the click by using the Invoke method. Notice I pause 1.5 seconds to give my application time to respond. I could also go into a delay loop, checking periodically to see if the result textBox2 field is empty or not. At this point in my test automation code, I have launched the application under test, entered "Hello!" into the input TextBox control, selected the DES Encrypt RadioButton control, and clicked on the Compute Button control.

Now I examine the TextBox2 control to see if I have a correct expected value:

Console.WriteLine("\nChecking TextBox2 for '91-1E-84-41-67-4B-FF-8F'"); TextPattern tpTextBox2 = (TextPattern)aeTextBox2.GetCurrentPattern(TextPattern.Pattern); string result = tpTextBox2.DocumentRange.GetText(-1);

Here I use TextPattern to prepare a call to a GetText method. Notice that I call GetText indirectly through a DocumentRange property, which returns a text range that encloses the main text of a document, in this case, a single text box. The -1 argument to GetText is used so that there is no maximum limit on the length of the return string. An alternative way to read the contents of the TextBox2 control is to use the GetCurrentPropertyValue method:

string result = (string)aeTextBox2.GetCurrentPropertyValue(ValuePattern.ValueProperty);

I have hardcoded the test case input into the test harness. A more flexible approach is to read test case input and expected values from some external data store. Now, with the actual value of the application under test in hand, I check against an expected value to determine my test scenario pass/fail result:

if (result == "91-1E-84-41-67-4B-FF-8F") { Console.WriteLine("Found it"); Console.WriteLine("\nTest scenario: Pass"); } else { Console.WriteLine("Did not find it"); Console.WriteLine("\nTest scenario: *FAIL*"); }

I simply display the test scenario result to the command shell. In a production environment, you will usually want to write test results to external storage.

With my test scenario completed, I can close the application under test by exercising the Menu control. First, I get the top-level File MenuItem control:

Console.WriteLine("\nClicking on File-Exit item in 5 seconds"); Thread.Sleep(5000); AutomationElement aeFile = null; aeFile = aeCryptoCalc.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "File")); if (aeFile == null) throw new Exception("Could not find File menu"); else Console.WriteLine("Got File menu");

Notice that I use the TreeScope.Descendants scope because the File MenuItem control is a subcontrol of the Menu container control. Now I simulate a user clicking on the File item:

Console.WriteLine("Clicking on 'File'"); ExpandCollapsePattern expClickFile = (ExpandCollapsePattern)aeFile.GetCurrentPattern(ExpandCollapsePattern.Pattern); expClickFile.Expand();

MenuItem controls that have submenus do not expose an Invoke pattern as you might expect; they expose an Expand pattern. With the File submenu items now rendered and visible, I can get a reference to the Exit command:

AutomationElement aeFileExit = null; aeFileExit = aeCryptoCalc.FindFirst(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, "Exit")); if (aeFileExit == null) throw new Exception("Could not find File-Exit"); else Console.WriteLine("Got File-Exit"); ;

And now I can use an InvokePattern on the Exit submenu to close the CryptoCalc application under test:

InvokePattern ipFileExit = (InvokePattern)aeFileExit.GetCurrentPattern(InvokePattern.Pattern); ipFileExit.Invoke(); Console.WriteLine("\nEnd automation\n");

At this point my test automation is done, and I can record my test case result and launch another test scenario.

Final Words

The code presented in this month's column gives you a good foundation for getting started with creating custom test automation for WPF applications. The MUIA library is quite extensive and can handle most simple testing scenarios.

The pattern to adapt the simple example I've presented here to test your own WPF application is straightforward. When creating your WPF app, try to ensure that all controls have a XAML Name attribute so that an AutomationID is generated. Use the UISpy tool to determine how to identify and manipulate user controls. Determine the MUIA Pattern that will allow you to examine the state and value of a user control. With this information in hand, you can handle most basic UI test automation scenarios.

In all testing situations, you must carefully weigh the effort required to create your test automation against the benefit you get from the automation. Based on my experience, WPF UI test automation is generally best used for regression testing of relatively simple scenarios. This allows you to concentrate your manual testing on complex scenarios and find new, subtle bugs without having to worry about missing an obvious bug that was accidentally introduced when development added new functionality.

As a general rule of thumb, for the type of lightweight test automation described in this column, I have found that if my test automation takes less than four hours to create, then I get a reasonable return of value on my time investment. Of course your environment will be different; the point is you should not automatically assume that UI test automation is always the best possible use of your testing resources. WPF is still a relatively new technology. But as the presence of WPF applications increases, the UI test automation techniques presented in this column are likely to become increasingly useful to you for creating better software.

Dr. James McCaffrey works for Volt Information Sciences, Inc., where he manages technical training for software engineers working at Microsoft's Redmond, Washington campus. He has worked on several Microsoft products including Internet Explorer and MSN Search. James is the author of .NET Test Automation Recipes(Apress, 2006). James can be reached at jmccaffrey@volt.comor v-jammc@microsoft.com.