November 2010

Volume 25 Number 11

Test Run - Web UI Test Automation with the WebBrowser Control

By James McCaffrey | November 2010

James McCaffreyIn this month’s column I show you a new way to create UI test automation for Web applications. The technique I present provides one solution to a very common but tricky testing scenario: how to deal with modal message boxes generated by a Web application.

The best way for you to see where I’m headed is to take a look at the screenshots in Figures 1 and 2. The image in Figure 1 shows a simplistic but representative Web application hosted in Internet Explorer. The application accepts user input into a text box, and after the user clicks on the button labeled Click Me, the app identifies the color of the input item, then displays the result in a second text box.

image: Example Web App Under Test

Figure 1 Example Web App Under Test

Notice that when the Click Me button is clicked, the application’s logic checks to see if the user input box is empty. If so, it generates a modal message box with an error message. Dealing with a message box is problematic in part because the message box isn’t part of the browser or the browser document.

Now take a look at the example test run in Figure 2. The test harness is a Windows Forms application. Embedded inside the Windows Forms app is a WebBrowser control that gives the Windows Forms the ability to display and manipulate the dummy Web application under test.

image: Example Test Run

Figure 2 Example Test Run

If you examine the messages in the ListBox control at the bottom of the Windows Forms harness, you’ll see that the test harness begins by loading the Web app under test into the WebBrowser control. Next, the harness uses a separate thread of execution to watch for—and deal with—any message boxes generated by the Web app. The harness simulates a user-click on the Web app Click Me button, which in turn creates a modal error message box. The watcher thread finds the message box and simulates a user clicking it away. The test harness concludes by simulating a user typing “roses” into the first input box, clicking on the Click Me button, and looking for the test case expected response of “red” in the second text box.

In this article, I briefly describe the example Web application under test. I then walk you through the code for the Windows Forms test harness so you’ll be able to modify my code to meet your testing scenarios. I conclude by describing situations where this technique is applicable and when alternative techniques may be better.

This article assumes you have basic Web development skills and intermediate C# coding skills, but even if you’re new to C# you should be able to follow without much difficulty. I think you’ll find the technique I present here a useful addition to your personal software testing, development and management tool kit.

The Application Under Test

Let’s take a look at the code for the example Web app that’s the target of my test automation. For simplicity I created the application using Notepad. The application functionality is supplied by client-side JavaScript rather than by server-side processing. As I’ll explain later, this test automation technique will work with applications based on most Web technologies (such as ASP.NET, Perl/CGI and so on), but the technique is best suited for applications that use JavaScript to generate message boxes. The entire Web application code is presented in Figure 3.

Figure 3 The Web App

<html>
<head>
<title>Item Color Web Application</title>
<script language="JavaScript">
  function processclick() {
    if (document.all['TextBox1'].value == "") {
      alert("You must enter an item into the first box!");
    }
    else {
      var txt = document.all['TextBox1'].value;
      if (txt == "roses")
        document.all["TextBox2"].value = "red";
      else if (txt == "sky")
        document.all["TextBox2"].value = "blue";
      else
        document.all["TextBox2"].value = "I don't know that item";
    }
  }
</script>
</head>
<body bgcolor="#F5DEB3">
  <h3>Color Identifier Web Application</h3>
  <p>Enter an item: 
    <input type="text" id="TextBox1" /></p>
  <p><input type="button" value="Click Me" 
            id="Button1" 
            onclick="processclick()"/></p>
  <p>Item color is: 
    <input type="text" id="TextBox2" /></p>
</body>
</html>

I saved my Web app as default.html in a directory named ColorApp located in the C:\Inetpub\wwwroot directory on my test host machine. To steer clear of security issues, the technique I present here works best when the test automation runs directly on the machine that acts as the Web server hosting the application under test. To keep my example Web app simple and not obscure details of the test automation, I took shortcuts you wouldn’t see in a production Web app, such as eliminating error checks.

The heart of the Web app’s functionality is contained in a JavaScript function named processclick. That function is called when a user clicks on the application’s button control with ID Button1 and label value “Click Me.” The processclick function first checks to see if the value in input element TextBox1 is empty. If so, it generates an error message box using the JavaScript alert function. If the TextBox1 input element is not empty, the processclick function uses an if-then statement to produce a value for the TextBox2 element. Notice that because the Web application’s functionality is provided by client-side JavaScript, the application doesn’t perform multiple client-to-server round-trips, and therefore the Web application is loaded only once per functionality cycle.

The Windows Forms Test Harness

Now let’s walk through the test harness code illustrated in Figure 2 so that you’ll be able to modify the code to meet your own needs. The test harness is a Windows Forms application; normally I’d use Visual Studio to create the program. However, I’m going to show you how to create the harness using Notepad and the command-line C# compiler because the ease of use and auto-generated code in Visual Studio hide some important concepts. Once you understand my example code you should have no trouble using Visual Studio rather than Notepad.

I open Notepad and begin my harness by declaring the namespaces used:

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Threading;

You need the System, Forms and Drawing namespaces for basic Windows Forms functionality. The InteropServices namespace allows the test harness to find and manipulate modal message boxes using the P/Invoke mechanism. P/Invoke allows you to create C# wrapper methods that call native Win32 API functions. The Threading namespace is used to spin off a separate thread that watches for the appearance of message boxes.

Next I declare a harness namespace and I begin the code for the main Windows Forms application class, which inherits from the System.Windows.Forms.Form class:

namespace TestHarness
{
  public class Form1 : Form
  {
    [DllImport("user32.dll", 
      EntryPoint="FindWindow",
      CharSet=CharSet.Auto)]
    static extern IntPtr FindWindow(
      string lpClassName,
      string lpWindowName);
. . .

Immediately inside the Form1 definition I place a class-scope attribute that allows the test harness to call the external FindWindow API function, which is located in user32.dll. The FindWindow API function is mapped to a C# method also named FindWindow, which accepts the internal name of a window control and returns an IntPtr handle to the control. The FindWindow method will be used by the test harness to get a handle to the message box generated by the Web application under test.

Next, I add two more attributes to enable additional Win32 API functionality:

[DllImport("user32.dll", EntryPoint="FindWindowEx",
  CharSet=CharSet.Auto)]
static extern IntPtr FindWindowEx(IntPtr hwndParent,
  IntPtr hwndChildAfter, string lpszClass, 
  string lpszWindow);
[DllImport("user32.dll", EntryPoint="PostMessage",
  CharSet=CharSet.Auto)]
static extern bool PostMessage1(IntPtr hWnd, uint Msg,
  int wParam, int lParam);

The C# FindWindowEx method associated with the FindWindowEx API function will be used to get a child control of the control found by FindWindow, namely the OK button on the message box. The C# PostMessage1 method associated with the PostMessage API function will be used to send a mouse-up and mouse-down message—in other words, a click—to the OK button.

Then I declare the three class-scope controls that are part of the Windows Forms harness:

private WebBrowser wb = null;
private Button button1 = null;
private ListBox listBox1 = null;

The WebBrowser control is a managed code wrapper around native code that houses the functionality of the Internet Explorer Web browser. The WebBrowser control exposes methods and properties that can be used to examine and manipulate a Web page housed in the control. The Button control will be used to launch the test automation, and the ListBox control will be used to display test harness logging messages.

Next I begin the code for the Form1 constructor:

public Form1() {
    // button1
    button1 = new Button();
    button1.Location = 
      new Point(20, 430);
    button1.Size = new Size(90, 23);
    button1.Text = "Load and Test";
    button1.Click += 
      new EventHandler(
      this.button1_Click);
  . . .

This code should be fairly self-explanatory. It’s quite possible that you’ve never had to write Windows Forms UI code like this from scratch before because Visual Studio does such a good job of generating UI boilerplate code. Notice the pattern: instantiate a control, set the properties and then hook up event handler methods. You’ll see this same pattern with the WebBrowser control. When using the test automation technique I present in this article, it’s useful to have a way to display logging messages, and a ListBox control works well:

// listBox1
listBox1 = new ListBox();
listBox1.Location = new Point(10, 460);
listBox1.Size = new Size(460, 200);

Next I set up the WebBrowser control:

// wb
wb = new System.Windows.Forms.WebBrowser();
wb.Location = new Point(10,10);
wb.Size = new Size(460, 400);
wb.DocumentCompleted +=
  new WebBrowserDocumentCompletedEventHandler(ExerciseApp);

The key thing to notice here is that I hook up an event handler method to the DocumentCompleted event so that, after the Web application under test is fully loaded into the WebBrowser control, control of execution will be transferred to a program-defined method named ExerciseApp (which I haven’t yet coded). This is important because in almost all situations there will be a delay while the Web application is loading, and any attempt to access the Web application in the control before it’s fully loaded will throw an exception.

You might have guessed that one way to deal with this is to place a Thread.Sleep statement into the test harness. But because the harness and WebBrowser control are both running in the same thread of execution, the Sleep statement will halt both the test harness and the WebBrowser loading.

I finish the Form1 constructor code by attaching the user controls to the Form object:

// Form1
  this.Text = "Lightweight Web Application Windows Forms Test Harness";
  this.Size = new Size(500, 710);
  this.Controls.Add(wb);
  this.Controls.Add(button1);
  this.Controls.Add(listBox1);
} // Form1()

Next I code the event handler method for the button control on the Windows Forms harness that kicks off the test automation:

private void button1_Click(object sender, EventArgs e) {
  listBox1.Items.Add(
    "Loading Web app under test into WebBrowser control");
  wb.Url = new Uri(
    "http://localhost/ColorApp/default.html");
}

After logging a message, I instruct the WebBrowser control to load the Web app under test by setting the control’s Url property. Notice that I’ve hardcoded the URL of the app under test. The technique I present here is best suited for lightweight, disposable test automation where hardcoded parameter values have fewer disadvantages than in situations where your test automation must be used over a long period of time.

Next I begin the code for the ExerciseApp method, which accepts control of execution when the DocumentCompleted event is fired:

private void ExerciseApp(object sender, EventArgs e) {
  Thread thread = new Thread(new
    ThreadStart(WatchForAndClickAwayMessageBox));
  thread.Start();

The ExerciseApp method contains most of the actual test harness logic. I begin by spawning a new thread associated with a program-defined method named WatchForAndClickAwayMessageBox. The idea here is that when a modal message box is generated by the Web application under test in the WebBrowser control, all test harness execution will halt until that message box is dealt with, meaning that the test harness can’t directly deal with the message box. So by spinning off a separate thread that watches for a message box, the harness can indirectly deal with the message box.

Next I log a message and then simulate a user clicking on the Web application’s Click Me button:

listBox1.Items.Add(
  "Clicking on 'Click Me' button");
HtmlElement btn1 = 
  wb.Document.GetElementById("button1");
btn1.InvokeMember("click");

The GetElementById method accepts the ID of an HTML element that’s part of the document loaded into the WebBrowser control. The InvokeMember method can be used to fire off events such as click and mouseover. Because there’s no text in the Web app’s TextBox1 control, the Web app will generate the error message box, which will be dealt with by the harness WatchForAndClickAwayMessageBox method, as I’ll explain shortly.

Now, assuming that the message box has been dealt with, I continue the test scenario:

listBox1.Items.Add("Waiting to click away message box");
listBox1.Items.Add("'Typing' roses into TextBox1");
HtmlElement tb1 = wb.Document.GetElementById("TextBox1");
tb1.InnerText = "roses";

I use the InnerText property to simulate a user typing “roses” into the TextBox1 control. Other useful properties for manipulating the Web application under test are OuterText, InnerHtml, and OuterHtml.

My automation continues by simulating a user click on the Web application’s Click Me button:

listBox1.Items.Add(
  "Clicking on 'Click Me' button again");
btn1 = wb.Document.GetElementById("button1");
btn1.InvokeMember("click");

Unlike the previous simulated click, this time there’s text in the TextBox1 control, so the Web application’s logic will display some result text in the TextBox2 control, and the test harness can check for an expected result and log a pass or fail message:

listBox1.Items.Add("Looking for 'red' in TextBox2");
HtmlElement tb2 = wb.Document.GetElementById("TextBox2");
string response = tb2.OuterHtml;
if (response.IndexOf("red") >= 0) {
  listBox1.Items.Add("Found 'red' in TextBox2");
  listBox1.Items.Add("Test scenario result: PASS");
}
else {
  listBox1.Items.Add("Did NOT find 'red' in TextBox2");
  listBox1.Items.Add("Test scenario result: **FAIL**");
}

Notice that the HTML response will look something like <input type=”text” value=”red” />, so I use the IndexOf method to search the OuterHtml content for the correct expected result.

Here’s the definition for the method that will deal with the Web app’s modal message box:

private void WatchForAndClickAwayMessageBox() {
  IntPtr hMessBox = IntPtr.Zero;
  bool mbFound = false;
  int attempts = 0;
  string caption = "Message from webpage";
. . .

I declare a handle to the message box, a Boolean variable to let me know when the message box has been found, a counter variable so I can limit the number of times my harness will look for the message box to prevent an endless loop, and the caption of the message box to look for. Although in this case the message box caption is fairly obvious, you can always use the Spy++ tool to verify the caption property of any window control.

Next I code a watching loop:

do {
  hMessBox = FindWindow(null, caption);
  if (hMessBox == IntPtr.Zero) {
    listBox1.Items.Add("Watching for message box . . . ");
    System.Threading.Thread.Sleep(100);
    ++attempts;
  }
  else {
    listBox1.Items.Add("Message box has been found");
    mbFound = true;
  }
} while (!mbFound && attempts < 250);

I use a do-while loop to repeatedly attempt to get a handle to a message box. If the return from FindWindow is IntPtr.Zero, I delay 0.1 seconds and increment my loop attempt’s counter. If the return is not IntPtr.Zero, I know I’ve obtained a handle to the message box and I can exit the do-while loop. The “attempts < 250” condition will limit the amount of time my harness is waiting for a message box to appear. Depending on the nature of your Web app, you may want to modify the delay time and the maximum number of attempts.

After the do-while loop exits, I finish up the WatchForAndClickAwayMessageBox method by seeing if the exit occurred because a message box was found or because the harness timed out:

if (!mbFound) {
  listBox1.Items.Add("Did not find message box");
  listBox1.Items.Add("Test scenario result: **FAIL**");
}
else { 
  IntPtr hOkBtn = FindWindowEx(hMessBox, IntPtr.Zero, null, "OK");
  ClickOn(hOkBtn );
}

If the message box wasn’t found, I classify this as a test case failure and log that result. If the message box was found, I use the FindWindowEx method to get a handle to the OK button child control located on the parent message box control and then call a program-defined helper method named ClickOn, which I define as:

private void ClickOn(IntPtr hControl) {
  uint WM_LBUTTONDOWN = 0x0201;
  uint WM_LBUTTONUP = 0x0202;
  PostMessage1(hControl, WM_LBUTTONDOWN, 0, 0);
  PostMessage1(hControl, WM_LBUTTONUP, 0, 0);
}

The Windows message constants 0201h and 0202h represent left-mouse-button-down and left-mouse-button-up, respectively. I use the PostMessage1 method that’s hooked to the Win32 PostMessage API function, which I described earlier.

My test harness ends by defining the harness Main method entry point:

[STAThread]
private static void Main() {
  Application.Run(new Form1());
}

After saving my test harness as Harness.cs, I used the command-line compiler. I launch the special Visual Studio command shell (which knows where the csc.exe C# compiler is), navigate to the directory holding my Harness.cs file, and issue the command:

C:\LocationOfHarness> csc.exe /t:winexe Harness.cs

The /t:winexe argument instructs the compiler to generate a Windows Forms executable rather than the default console application executable. The result is a file named Harness.exe, which can be executed from the command line. As I mentioned earlier, you will likely want to use Visual Studio rather than Notepad to create WebBrowser control-based test automation.

Wrapping Up

The example I’ve presented here should give you enough information to get you started writing your own WebBrower control-based test automation. This technique is best suited for lightweight automation scenarios—situations where you want to get your test automation up and running quickly and where the automation has a short expected lifespan. The strength of this technique is its ability to deal with modal message boxes—something that can be quite tricky when using other UI test-automation approaches. This technique is especially useful when you’re testing Web application functionality that’s generated primarily by client-side JavaScript.

In situations where Web application functionality is generated by multiple client-server round trips, you’ll have to modify the code I’ve presented, because each time a response is returned from the Web server, the DocumentCompleted event will be fired. One approach for dealing with this is to create and use a variable that tracks the number of DocumentCompleted events and adds branching logic to your harness.                 


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

Thanks to the following Microsoft technical experts for reviewing this article: Paul Newson and Dan Liebling