测试运行

使用 WebBrowser 控件自动进行 Web UI 测试

James McCaffrey

下载代码示例

在本月的专栏中,我将向您介绍为 Web 应用程序创建 UI 测试自动化的新方法。我要演示的方法适用于一种很常见却又很棘手的情况:如何处理 Web 应用程序生成的模式消息框。

要想了解我将讲述的内容,最好是看一下图 12 中的屏幕快照。图 1 中的图像是 Internet Explorer 中托管的简单但很有代表性的 Web 应用程序。该应用程序接受用户在文本框中的输入,在用户单击标有“Click Me”的按钮后,该应用程序标识输入项的颜色,然后在第二个文本框中显示结果。

图 1 待测试的示例 Web 应用程序

请注意,当用户单击“Click Me”按钮后,应用程序的逻辑会检查用户输入框是否为空。如果为空,该应用程序会生成一个模式消息框,并显示一条错误消息。消息框不易处理,部分原因在于,消息框不属于浏览器,也不属于浏览器文档。

现在,让我们看一下图 2 中的示例测试运行。测试工具是 Windows 窗体应用程序。Windows 窗体应用程序中嵌入了一个 WebBrowser 控件,Windows Forms 通过该控件显示和处理待测试的虚拟 Web 应用程序。

图 2 示例测试运行

如果在 Windows 窗体工具底部查看 ListBox 控件中的消息,可以看到,该测试工具首先将待测试的 Web 应用程序加载到 WebBrowser 控件中。接下来,该工具使用单独的执行线程来监视并处理 Web 应用程序生成的所有消息框。该工具模拟用户单击 Web 应用程序的“Click Me”按钮,从而创建模式错误消息框。观察程序线程查找消息框并模拟用户单击,使消息框消失。最后,测试工具模拟用户在第一个输入框中键入“roses”,单击“Click Me”按钮,然后在第二个文本框中查找测试用例预期的响应“red”。

在本文中,我将简要介绍待测试的示例 Web 应用程序。然后,将逐步介绍 Windows 窗体测试工具的代码,以便您根据测试方案的需要,对这些代码进行修改。最后,我将介绍哪些情况下适用此方法,哪些情况下适用其他方法。

本文假设您具备基本的 Web 开发技能和中级 C# 编码技能,不过,即使您是 C# 初学者,也应该能理解相关内容。相信您会发现,本文中介绍的方法是对您的个人软件测试、开发和管理工具包的有用补充。

待测试的应用程序

我们来看看作为测试自动化目标的示例 Web 应用程序的代码。为简单起见,我使用记事本来创建应用程序。应用程序功能由客户端 JavaScript 而不是服务器端处理来提供。稍后我将介绍,此测试自动化方法适用于基于大多数 Web 技术(如 ASP.NET、Perl/CGI 等)的应用程序,但最适用于使用 JavaScript 生成消息框的应用程序。完整的 Web 应用程序代码如图 3 所示。

图 3 Web 应用程序

<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>

我将 Web 应用程序命名为 default.html,保存在测试主机上 C:\Inetpub\wwwroot 目录中的 ColorApp 目录中。 为了避免出现安全问题,如果测试自动化直接在充当托管待测试应用程序的 Web 服务器的计算机上运行时,此处介绍的方法效果最佳。 为了使 Web 应用程序简单又能说明测试自动化的详细信息,我走了些捷径(如不进行错误检查),这些做法是不会在生产 Web 应用程序中出现的。

Web 应用程序功能的核心包含在名为 processclick 的 JavaScript 函数中。 当用户单击应用程序中 ID 为 Button1 且标记值为“Click Me”的按钮控件时,将调用该函数。Processclick 函数首先检查输入元素 TextBox1 中的值是否为空, 如果为空,则使用 JavaScript 警告函数生成错误消息框。 如果 TextBox1 输入元素不为空,则 processclick 函数使用 if-then 语句为 TextBox2 元素生成值。 请注意,Web 应用程序的功能由客户端 JavaScript 提供,并且应用程序不执行多次客户端到服务器的往返,因此,只在每个功能循环中加载一次 Web 应用程序。

Windows 窗体测试工具

现在,我们将逐步介绍图 2 所示的测试工具代码,以便您根据自己的需要修改代码。 测试工具是一个 Windows 窗体应用程序;通常我会使用 Visual Studio 来创建该程序。 Visual Studio 中自动生成的代码很方便,但会隐藏一些重要概念,因此,我将向您演示如何使用记事本和命令行 C# 编译器创建工具。 只要理解了我的示例代码,则使用 Visual Studio 代替记事本不会遇到任何问题。

我打开记事本,通过声明所使用的命名空间开始创建工具:

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

您需要 System、Forms 和 Drawing 命名空间来实现基本的 Windows 窗体功能。 通过使用 InteropServices 命名空间,测试工具可以使用 P/Invoke 机制查找并处理模式消息框。 P/Invoke 可用于创建调用本机 Win32 API 函数的 C# 包装方法。 Threading 命名空间用于衍生一个单独的线程来监视消息框的外观。

接下来,声明一个工具命名空间,开始编写主 Windows 窗体应用程序类的代码,该类继承自 System.Windows.Forms.Form 类:

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

在 Form1 定义中的开始处,我放置了一个类作用域的属性,该属性允许测试工具调用位于 user32.dll 中的外部 FindWindow API 函数。 FindWindow API 函数会映射到名称同为 FindWindow 的 C# 方法,该方法将接受窗口控件的内部名称并返回该控件的 IntPtr 句柄。 测试工具将使用 FindWindow 方法获取待测试的 Web 应用程序生成的消息框的句柄。

接下来,再添加两个属性,以启用其他 Win32 API 功能:

[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);

与 FindWindowEx API 函数关联的 C# FindWindowEx 方法将用于获取 FindWindow 所找到的控件的子控件,即消息框中的 OK 按钮。 与 PostMessage API 函数关联的 C# PostMessage1 方法将用于发送鼠标按钮弹起和按下消息,即单击 OK 按钮。

然后,声明三个属于 Windows 窗体工具的类作用域控件:

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

WebBrowser 控件是本机代码的托管代码包装,用于容纳 Internet Explorer Web 浏览器的功能。 WebBrowser 控件公开可对该控件中容纳的网页进行检查和处理的方法和属性。 Button 控件将用于启动测试自动化,ListBox 控件将用于显示测试工具记录消息。

接下来,开始编写 Form1 构造函数的代码:

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);
  .
.
.

此代码应该是非常容易理解的。 Visual Studio 能够出色地生成 UI 样板代码,因此,您可能从未像这样从头开始编写 Windows 窗体 UI 代码。 请注意处理方式:实例化控件,设置属性,然后挂接事件处理程序方法。 WebBrowser 控件的处理方式也是如此。 使用本文所述的测试自动化方法时,通过某种方式显示记录消息会很有用,ListBox 控件恰好可提供该功能:

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

接下来,设置 WebBrowser 控件:

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

此处应注意,我将事件处理程序方法挂接到 DocumentCompleted 事件,因此在将待测试的 Web 应用程序完全加载到 WebBrowser 控件中之后,执行控制将转移给程序定义的方法 ExerciseApp(我还没有编写其代码)。 这一点很重要,因为在几乎所有情况下,加载 Web 应用程序都存在延迟,在应用程序完全加载之前,任何访问该控件中的 Web 应用程序的尝试都会引发异常。

您可能已经猜出一种可以处理此情况的方法,即在测试工具中放置一条 Thread.Sleep 语句。 但是,由于测试工具和 WebBrowser 控件都在同一执行线程中运行,因此 Sleep 语句将同时停止测试工具和 WebBrowser 加载。

我通过向 Form 对象附加用户控件完成了 Form1 构造函数的代码:

// 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()

接下来,为 Windows Forms 工具中用于启动测试自动化的按钮控件编写事件处理程序方法的代码:

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");
}

在记录消息后,我通过设置 WebBrowser 控件的 Url 属性来指示该控件加载待测试的 Web 应用程序。 请注意,我已经对待测试应用程序的 URL 进行了硬编码。 此处介绍的方法最适合轻量型、可释放的测试自动化,与必须在较长时间段内使用测试自动化的情况相比,硬编码参数值的缺点更少。

接下来,开始编写 ExerciseApp 方法的代码,该方法将在引发 DocumentCompleted 事件时接受执行的控制:

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

ExerciseApp 方法包含大部分实际的测试工具逻辑。 首先生成与程序定义的方法 WatchForAndClickAwayMessageBox 关联的新线程。 具体思路是,当 WebBrowser 控件中待测试的 Web 应用程序生成模式消息框时,所有测试工具的执行都将停止,直到该消息框得到处理为止,这意味着测试工具无法直接处理消息框。 因此,通过衍生可监视消息框的单独线程,该工具可间接处理消息框。

接下来,记录一条消息,然后模拟用户单击 Web 应用程序的“Click Me”按钮:

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

GetElementById 方法接受加载到 WebBrowser 控件中的文档所包含的 HTML 元素的 ID。 InvokeMember 方法可用于触发事件(如单击和鼠标悬停)。 Web 应用程序的 TextBox1 控件中没有文本,因此,Web 应用程序将生成错误消息框,工具的 WatchForAndClickAwayMessageBox 方法将处理该消息框,稍后将对此进行介绍。

现在,假定消息框已得到处理,下面继续测试方案:

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";

我使用 InnerText 属性模拟用户在 TextBox1 控件中键入“roses”。 其他可用于处理待测试 Web 应用程序的有用属性包括 OuterText、InnerHtml 和 OuterHtml。

下面通过模拟用户单击 Web 应用程序的“Click Me”按钮来继续自动化过程:

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

与前面的模拟单击不同,此时 TextBox1 控件中有文本,因此,Web 应用程序的逻辑将在 TextBox2 控件中显示某些结果文本,测试工具可以检查预期结果并记录通过或失败消息:

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**");
}

请注意,HTML 响应将类似于 <input type=”text” value=”red” />,因此,我使用 IndexOf 方法在 OuterHtml 内容中搜索正确的预期结果。

下面是 Web 应用程序模式消息框的处理方法的定义:

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

我声明了一个消息框句柄;一个 Boolean 变量(在发现消息框时发出通知),一个计数器变量(用来限制测试工具查找消息框的次数,以防止无限循环),以及要查找的消息框的标题。 尽管本示例中的消息框标题很明显,任何时候都可以使用 Spy++ 工具来验证任何窗口控件的标题属性。

接下来,编写监视循环编码:

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);

我使用一个 do-while 循环重复尝试获取消息框句柄。 如果从 FindWindow 返回的结果为 IntPtr.Zero,则延迟 0.1 秒并使循环尝试计数器递增。 如果返回的结果不是 IntPtr.Zero,说明已获得消息框句柄,可以退出 do-while 循环。 “attempts < 250”条件将限制测试工具等待消息框出现的时间。 根据 Web 应用程序的特性,可能需要修改延迟时间和最大尝试次数。

退出 do-while 循环后,通过确定退出原因是发现消息框还是工具超时,就完成了 WatchForAndClickAwayMessageBox 方法:

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 );
}

如果未发现消息框,则将此归类为测试用例失败并记录结果。 如果发现消息框,则使用 FindWindowEx 方法获取父消息框控件上的 OK 按钮子控件的句柄,然后调用程序定义的帮助器方法 ClickOn,其定义如下:

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);
}

Windows 消息常量 0201h 和 0202h 分别表示按下鼠标左键和鼠标左键弹起。 这里使用挂接到前述 Win32 PostMessage API 函数的 PostMessage1 方法。

我的测试工具以定义工具 Main 方法入口点作为结束:

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

将测试工具保存为 Harness.cs 之后,我使用了命令行编译器。 启动特定的 Visual Studio 命令 Shell(它知道 csc.exe C# 编译器的位置),导航到 Harness.cs 文件所在的目录,然后发出以下命令:

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

/t:winexe 参数指示编译器生成 Windows 窗体可执行文件,而不是默认的控制台应用程序可执行文件。结果是名为 Harness.exe 的文件,可从命令行执行该文件。如前所述,您可能希望使用 Visual Studio 而不是记事本来创建基于 WebBrowser 控件的测试自动化。

总结

本文介绍的示例提供了足够的信息,可以帮助您开始编写自己的基于 WebBrower 控件的测试自动化程序。此方法最适合轻量自动化方案,在这类方案中,您需要快速启动并运行测试自动化,并且自动化的预期生命周期较短。这种方法的优势是可以处理模式消息框,这是其他 UI 测试自动化方法难于轻松处理的。在测试主要由客户端 JavaScript 生成的 Web 应用程序功能时,此方法特别有用。

如果 Web 应用程序功能是通过多次客户端-服务器往返生成的,您必须对本文中的代码进行修改,因为每当从 Web 服务器返回响应时,都会触发 DocumentCompleted 事件。一种处理方法是创建并使用一个变量,以跟踪 DocumentCompleted 事件数并向工具添加分支逻辑。

James McCaffrey 博士供职于 Volt Information Sciences, Inc.,在该公司他负责管理对华盛顿州雷蒙德市沃什湾 Microsoft 总部园区的软件工程师进行的技术培训。他参与过多项 Microsoft 产品的研发工作,包括 Internet Explorer 和 MSN Search。McCaffrey 博士是《.NET 软件测试自动化之道》(Apress,2006)一书的作者,您可以通过以下地址与他联系:jammc@microsoft.com

衷心感谢以下 Microsoft 技术专家对本文的审阅:Paul NewsonDan Liebling