Web 应用程序用户界面低层测试自动化
 

2009-12-18 作者:James McCaffrey 翻译:biny_yang 来源:vckbase.com

 

下载源代码:TestRun0510.exe (166KB)

原文出处:Low-Level Web App UI Test Automation

Web 应用程序日渐复杂,对它们的测试工作也变得越来越重要。有很多测试技术可供你选择使用。例如,在 2005 年 4 月份的 MSDN 杂志中,我描述了一个基于 JScript 的简单系统,它使用 IE 的文档对象模型(Internet Explorer Document Model)来完整地测试某个 Web 应用程序的用户界面。这个技术很有效,但是在几个方面存在着缺陷。我的一些同事问我是否能使用 .Net 框架编写功能更强,但仍然是轻量级的 Web 应用程序用户界面自动化测试程序。在这个月的专栏文章中,我将向你们展示怎样达到这个目的。这个低层技术要求直接调用 fmshtml.dll 和 shdocvw.dll 动态链接库来访问和操纵 IE 浏览器客户区中的 HTML 对象。

让我们从一个屏幕截图开始,如图1所示。图1表明我正在测试一个虚构的 Web 应用程序,它搜索一个雇员信息数据仓库。用户能通过雇员的姓和名进行搜索。应用程序显示雇员的姓、名,如果大小写都匹配,那么显示相应的生日。通过手工测试这个 Web 应用程序的用户界面是非常沉闷的、低效的,并且可能错误百出。更好的一种方法是通过测试自动化。该自动化启动一个IE实例,装载正在进行的测试,操纵应用程序,并检查应用程序的正确性状态。

图1 测试执行例子

当然,一个实际的 Web 应用程序肯定比这更加复杂,但是我向你们展示的技术能够用来测试基于 IE 的 Web 应用程序。在接下来的一节中,我将简要介绍正进行测试的 Web 应用程序,这样你就能理解我在测什么和怎么测。我也会详细解释产生图1的测试场景代码,也会描述怎样改编和扩展这里描述的技术。

被测试的 Web 应用程序

我的 Web 应用程序,WebForm1.aspx,是一个ASP.NET程序,但是本专栏描述的技术能够应用于任何类型的 Web 应用程序中。我的程序包括:两个单选按钮控件来告诉应用程序的控制逻辑要搜索的是哪一种字段;一个文字输入控件来接受用户的搜索项;一个按钮控件来启动搜索;以及一个文本区,用来显示结果。在文本区的下方,有一个标志栏,它显示“搜索完成”,它的初始状态是隐藏。我这里阐述的技术的两个优点是:我不用对该Web应用程序进行插装,也不需要接触应用程序的源代码。但是,我需要知道Web应用程序中各种HTML元素的ID号,通过View/Source,我能轻易做到这一点(不过要求ID必须是静态的,不能是动态的)。例如,按钮控件有ID号“Button1”,而“Last Name单选按钮”控件有ID号“RadioButtonList1_0”。当然,如果你能够接触程序的源代码,你肯定已经有了这些控件的ID信息。

我使用Visual Studio.NET来创建这个用来测试的虚构的Web应用程序。从Visual Studio.NET的设计角度来看,我添加了三个标签控件、一个单选按钮、一个文本区、一个按钮控件和一个列表框控件。为简化起见,我使用了控件的默认名称“Label1”、“TetBox1”等。相关代码列在图2中。我声明了一个Employee类和一个ArrayList对象来存放Employee对象。在方法Page_Load中,我向ArrayList中增加了虚构的雇员数据。在实际的应用程序中,你的数据可能来自于SQL Server数据库或XML文件。但就用户界面测试自动化来说,数据来自哪里是无关紧要的。

图2

Figure 2 Code for Web Application Under Test
public class WebForm1 : System.Web.UI.Page
{
    ... // controls declared here

    public class Employee
    {
      public string last;
      public string first;
      public string dob;

      public Employee(string last, string first, string dob)
      {
        this.last = last;
        this.first = first;
        this.dob = dob;
      }
    }

    private ArrayList al = new System.Collections.ArrayList();

    private void Page_Load(object sender, System.EventArgs e)
    { 
      Employee e1 = new Employee("Adams","Terry","01/01/1971");
      Employee e2 = new Employee("Burke","Brian","02/02/1972");
      Employee e3 = new Employee("Ciccu","Alice","03/03/1973");

      al.Add(e1);
      al.Add(e2);
      al.Add(e3);

      Label3.Visible = false;
    }

    ... // Web Form Designer generated code   

    private void Button1_Click(object sender, System.EventArgs e)
    {
      ListBox1.Items.Clear();
      string filter = TextBox1.Text;

      if (RadioButtonList1.SelectedValue == "Last Name")
      {
        foreach (Employee emp in al)
        {
          if (emp.last.IndexOf(filter) >= 0)
            ListBox1.Items.Add(
              emp.last + ", " + emp.first + ", " + emp.dob);
        }
      }
      else if (RadioButtonList1.SelectedValue == "First Name")
      {
        foreach (Employee emp in al)
        {
          if (emp.first.IndexOf(filter) >= 0)
            ListBox1.Items.Add(
              emp.last + ", " + emp.first + ", " + emp.dob);
        }
      }
  
      Label3.Visible = true;
    }

方法Button1_Click中清空列表框控件,从文本框控件中获取过滤子串,检查单选按钮以便确定是搜索姓还是搜索名,在内存数据中搜索相匹配的,并把匹配的雇员信息显示出来。我得强调一下,为了使应用程序例子简单,我在这里可能用了一些不好的编码技术。这与你可能遇到的某种情况非常类似——你所处理的应用程序是发布前的,代码可能并没有经过优化。我的Web应用程序显然是虚构的,但是通过用户界面来测试任何Web应用程序的基本要素是应用程序随着每一个HTTP请求-应答对的状态转换。换句话说,就算你打算测的Web应用程序要求访问SQL Server数据库或者进行了非常复杂的处理,它只是一种状态转换,并且这种转换会在HTTP应答和用户界面中显示出来。

测试自动化

测试场景系统由一个单一文件组成。我打算把我的测试集实现为一个C#控制台应用程序,但你将会看到,我能使用任何与.Net兼容的语言 (例如,Visual Basic .NET),该技术也能用于任何程序(例如,一个Windows程序)和测试框架(例如,NUnit)。场景的整体结构显示在图3中。首先,我向“Microsoft Internet Controls”这个优秀的COM组件添加了一个引用(reference)。这是shdocvw.dll模块的一个别名,该模块拥有操作基于Windows的浏览器(例如IE和Windows Explorer)的能力。然后我向Microsoft.mshtml.Net组件添加了一个引用。这是mshtml.dll模块的一个别名,该模块拥有访问HTML元素的能力。我向两个相应的名字空间增加了“using”声明,这样我就不需完整验证他们的类了。我也针对System.Diagnostics和System.Threading分别增加了“using”声明,这样我容易引用前者的Process类,也能在我合适的时候引用后者的Thread.Slepp方法来暂停我的自动化过程。

图3

Figure 3 Test Scenario Structure 
using System;
using System.Threading;
using System.Diagnostics;
using SHDocVw;
using mshtml;

namespace RunTest
{
  class Class1
  {
    static AutoResetEvent documentComplete = new AutoResetEvent(false);

    [STAThread]
    static void Main(string[] args)
    {
      try
      {
        // Launch IE
        // Attach InternetExplorer object
        // Establish DocumentComplete event handler
        // Load app under test
        // Manipulate the app
        // Check the app's state
        // Log 'pass' or 'fail'
        // Close IE
      }
      catch(Exception ex)
      {
        Console.WriteLine("Fatal error: " + ex.Message);
      }
    }

    private static void ie_DocumentComplete(object pDisp, ref object URL)
    {
      documentComplete.Set();
    }
  }
}

这项技术的一个关键是要有能力确定一个Web页面/文档/应用程序何时已经在IE中充分装载了。我定义了一个类范围的AutoResetEvent对象documentComplet,我使用该对象来标记一个已经充分装载了文档的等待线程。

static AutoResetEvent documentComplete = new AutoResetEvent(false);

马上,我就会详细介绍这里的细节。我由向命令行中打印一条状态信息来开始我的测试场景。然后我声明了一个Boolean类型的变量pass并把它设为false。我假设测试场景会失败,如果我检测到应用程序的最后状态是正确的,我修正我的假设并把pass变量设为true。下一步我声明了一个InternetExplore对象“ie”:

Console.WriteLine("\nStart test run");
bool pass = false;
InternetExplorer ie = null;
InternetExplorer类是在SHDocVw名字空间中定义的。该类有很多方法操纵Internet Explorer的一个实例,但是由你决定启动Internet Explorer并把两者相关联,如下所示:
// launch explorer
Console.WriteLine("\nLaunching an instance of IE");
Process p = Process.Start("iexplore.exe", "about:blank");
if (p == null) throw new Exception("Could not launch IE");
Console.WriteLine("Process handle = " + p.MainWindowHandle.ToString());

// find all active browsers
SHDocVw.ShellWindows allBrowsers = new SHDocVw.ShellWindows();
Console.WriteLine("Number active browsers = " + allBrowsers.Count);
if (allBrowsers.Count == 0) throw new Exception("Cannot find IE");

我使用System.Diagnostics.Process名字空间中的Start方法来启动一个Internet Explorer(iexplore.exe)并装载空白页面“about:blank”;Start方法返回对创建进程对象的一个引用。然后我初始化了一个名为allBrowsers的ShellWindows对象。这个对象掌握了对所有ShellWindows对象的引用,也掌握了对浏览器的引用(包括Windows Explorer的实例,我刚才启动的Internet Explorer的实例和之前启动的Internet Explorer的实例)。我使用Count属性来显示关于目前活动浏览器的诊断信息,以便确保Internet Explorer成功启动了。测试自动化的下一步是把新的进程与Internet Explorer对象关联起来:

Console.WriteLine("Attaching to IE");
for(int i=0; i < allBrowsers.Count && ie == null; i++)
{
  InternetExplorer e = (InternetExplorer)allBrowsers.Item(i);
  if (e.HWND == (int)p.MainWindowHandle) ie = e;
}
if (ie == null)  throw new Exception("Failed to attach to IE");

可能有好几个Internet Explorer的实例正在运行,所以我需要辨明哪个是我的测试场景启动的,以便我能把我的InternetExplorer变量ie与正确的实例关联起来。记住,我把测试启动的Internet Explorer捕获到一个进程对象p中了。所以我遍历ShellWindows中的每一个对象,检查他们的句柄或指针是否和测试启动的进程的主窗口句柄一致。我有时候采用的一个替换的方法是假设只有我的测试Internet Explorer实例允许运行。如果有多个Internet Explorer实例运行,我抛出一个异常。这个假设运行我把测试Internet Explorer与下面的代码简单关联起来:

ie = (InternetExplorer)allBrowsers.Item(0);

你实际采用何种方法取决于你的实际测试场景。既然我建立了我的测试InternetExplorer对象,我能注册我之前提到的DocumentComplete事件句柄:

ie.DocumentComplete += new
  DWebBrowserEvents2_DocumentCompleteEventHandler(ie_DocumentComplete);

简单来说,当InternetExplorer的DocumentComplete事件发生时,调用用户定义的ie.DocumentComplete方法。如果你回头去看图3中的代码,你能看到我如此定义了该方法:

private static void ie_DocumentComplete(object pDisp, ref object URL)
{
  documentComplete.Set();
}

ie_DocumentComplete 方法调用了我早些时候在测试类中定义的AutoResetEvent对象中的Set方法。简而言之,现在我能暂停我的执行线程,直到我的InternetExplorer对象充分装载了。我会立即向你展示怎样具体做这件事情。现在我浏览正在进行测试的Web应用程序,等到应用程序充分装载:

Console.WriteLine("\nNavigating to the Web app");
object missing = Type.Missing;
ie.Navigate("http://localhost/LowLevelWebUIAutomationApp/WebForm1.aspx",
  ref missing, ref missing, ref missing, ref missing);
documentComplete.WaitOne();

我使用InternetExplorer.Navigate方法来装载我的Web应用程序。Navigate接受数个可选参数,但是在这个例子中,我不需要任何参数。注意,我调用了documentComplete对象的WaitOne方法。WaitOne将停止我的执行线程,直到应用程序充分装载到了Internet Explorer中。在这个例子中,我没有提供一个超时值,所以我会不停的等待,但你很可能会向WaitOne传递一个代表超时毫秒数的整型值。下一步我设定Internet Explorer为一个固定的大小,并获得Web应用程序文档的一个引用。

Console.WriteLine("Setting IE to 525x420");
ie.Width = 525;
ie.Height = 420;
HTMLDocument theDoc = (HTMLDocument)ie.Document;

我声明了一个HTMLDocument变量,并为它指定了一个值。HTMLDocument接口是在mshtml名字空间中定义的。我怎么知道呢?图4是Visual Studio .NET对象浏览器的一个屏幕截图。我扩展mshtml interop到汇编层,看到了它们的所有接口、类、事件和其他对象。

图 4对象浏览器

下一步,我模拟了对“Last Name”单选按钮的检查,向文本框控件中输入“urk”:

Console.WriteLine(
   "\nSelecting ''Last Name'' radio button");
HTMLInputElement radioButton = 
   (HTMLInputElement)theDoc.getElementById("RadioButtonList1_0");
radioButton.@checked = true;

Console.WriteLine("Setting text box to ''urk''");
HTMLInputElement textBox = 
  (HTMLInputElement)theDoc.getElementById("TextBox1");
textBox.value = "urk";

这两段代码是非常相似的,并且看起来相当明白。我通过getElementByID方法获得了HTMLInputElemen对象的引用。在拥有这个对象之后,我能使用它的属性或方法来操纵它。这里我选择单选按钮控件的“checked”属性(因为在C#中checked是一个保留字,我必须使用“@checked”)和文本框控件的“value”属性。按你下面看到的方式点击Search按钮:

Console.WriteLine("Clicking search button");
HTMLInputElement button =
  (HTMLInputElement)theDoc.getElementById("Button1");
button.click();
documentComplete.WaitOne();

在这个例子中,我需要调用WaitOne方法来确保表示搜索结果的页面被充分装载了。通过一些很小的实验,你会发现你能虚拟操纵任何HTML元素。例如,我能模拟下拉框的选择、超链接的点击,当然在这个测试场景中我无需这样做。在我操作Web应用程序之后,我必须检查最终状态的正确性。

Console.WriteLine("\nSeeking ''Burke, Brian'' in list box");
HTMLSelectElement selElement =
  (HTMLSelectElement)theDoc.getElementsByTagName(
    "select").item(0, null);
if (selElement.innerText.ToString().IndexOf("Burke, Brian") >= 0)
{
  Console.WriteLine("Found target string");
  pass = true;
}
else
{
  Console.WriteLine("*Target string not found*");
}

一般模式是通过共同的标签名获得一个HTML元素集的引用,然后使用属性得到某一特定元素,然后获取表示该元素头标签和尾标签之间数据串的innerText。这里,我获得了所有<select>元素的引用,然后通过元素集的属性得到了第一个<select>元素——当然,也是我的Web页面中唯一的<select>元素。这是由一个ASP.NET列表框控件生成的HTML。传递给项目属性的参数有点复杂。第一个参数既可以是整数,也可以是字符串,前者被理解为基于0的索引值,后者被理解为标签名。我向项目属性的第二个参数传递一个空值NULL。这个参数也是一个索引值,但它只是在项目属性返回一个集合而不是一个原子对象时才有用。有时你需要访问不是任何HTML字元素中的文档体中的值,下面的代码片断向你展示了一种达到目的的方法:

Console.WriteLine("Seeking ''Search complete'' in body");
HTMLBody body = (HTMLBody)theDoc.getElementsByTagName(
  "body").item(0, null);
if (body.createTextRange().findText("Search complete", 0, 0) == true)
{
  Console.WriteLine("Found target string");
  pass = true;
} 
else
{
  Console.WriteLine("*Target string not found*");
} 

我获得了文档体的一个引用,使用从createTextRange返回的IHTMLTxtRange对象的findText方法来搜索一个目标串。这两个参数0表示从“范围的开始”搜索,匹配部分串。启动Internet Explorer之后,装载测试中的Web应用程序,操作程序界面,检查程序的状态,剩下来需要做的就是决定测试结果是正确还是错误,还有要关闭Internet Explorer。

if (pass) Console.WriteLine("\nTest result = Pass\n");
else Console.WriteLine("\nTest result = *FAIL*\n");

Console.WriteLine("Closing IE in 3 seconds . . . ");
Thread.Sleep(3000);
ie.Quit();

Console.WriteLine("\nEnd test run");

在这个案例中,我简单的把我的测试结果记录在命令行中,你可能要把你的测试结果写到一个文本文件、XML文件或者SQL Server数据库中。

自动化扩展

我在这里阐述的这个简单的低层Web应用程序用户界面测试自动化技术的代码,你能从本专栏附属的代码中下载到。你能从几个方面扩展这个技术。一个显著的加强是使测试场景完全是自动化的。因为测试系统创建了一个.exe文件,你能轻易安排它在不需要手工交互的情况下运行,例如,使用Windows任务计划。你也可能使用System.Web.Mail名字空间(如果你使用的是.NET构架2.0,你将使用System.Net.Mail名字空间),通过e-mail发送测试运行结果总结。我在这里阐述的测试系统是一个简单的程序。虽然简单有效,但是测试集的重用却是困难的。你可以把这些基本路径提炼到一个.Net类库中。

但我在这个专栏中讨论一项技术时,我常省略了很多错误检测,你也可以写出你自己的错误检测语句,向你的测试集增加一些更细微的异常处理块。另外,为清晰起见,我的输入参数是严格限定的。你可以通过脚本来更灵活的指定它们,如果用一个XML文件来驱动测试则是更好的方法。

我阐述的方法的两个特点是:你无需接触Web应用程序的源代码;由于你工作在一个很低的层次,你能充分控制应用程序的用户界面。随着Web应用程序系统在复杂性上增加,测试你的软件比以前更重要。我这里描述的Web应用程序用户界面测试在你的产品测试中能起一个很大的作用。

火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。

资源网站: UML软件工程组织