下载代码示例

jQuery 库是一个开放的 JavaScript 函数源集合。尽管在创建 jQuery 时考虑了 Web 开发,但库有几个特性使其非常适合于轻型 Web 应用程序 UI 测试自动化。在本月的专栏中,我将为您演示如何做到这一点。

要想了解我将讲述的内容,最好是看一下 图 1 中的屏幕快照,图中演示了实际运行的使用 jQuery 实现的 UI 测试自动化。测试工具由 Internet Explorer 承载,并包含名为 UITestHarness.html 的 HTML 页面。

工具页面实际上就是一个带有两个 HTML frame 元素的容器。右侧的框架保留了待测试的 Web 应用程序,本例中将介绍一个名为 MiniCalc 的 ASP.NET 计算器应用程序,该应用程序很简单但具有代表性。左侧的框架保留了一个名为 TestScenario001.html 的 HTML 页面,该页面包含一个用于显示进度消息的 TextArea 元素、一个用于手动启动自动化的 Button 元素以及一些基于 jQuery 的 JavaScript 函数,这些函数用于操作待测试的 Web 应用程序并检查该应用程序的结果状态,以确定结果是通过还是失败。

图 1 使用 jQuery 进行 UI 测试自动化

jQuery 库也非常适于 HTTP 请求/响应测试,而我在 2010 年 1 月的“测试运行”专栏 ( msdn.microsoft.com/magazine/ee335793 ) 中使用了 jQuery 来处理请求/响应测试。

本文假定您已基本熟悉 ASP.NET 技术和中级 JavaScript 编程技能,并假定您没有使用 jQuery 库的任何经验。然而,即使您是刚刚接触 ASP.NET 和测试自动化,本月专栏所介绍的内容对您来说应该也不难理解。

在下面几节中,我将首先介绍 MiniCalc 应用程序,从而使您准确理解待测试的应用程序的实现与 UI 测试自动化的关系。接下来,我将向您介绍创建基于 jQuery 的轻型 UI 测试自动化的详细信息,如 图 1 所示。最后我将说明如何扩展我所介绍的技术来满足您自己的需求,并将讨论 jQuery UI 测试自动化与其他方法相比的优势和劣势。我相信此处介绍的技术相当有趣,并且可以成为您的测试、开发和管理工具集的有用补充。

待测试的应用程序

我们来看看作为基于 jQuery 的 UI 测试自动化的目标的 MiniCalc ASP.NET Web 应用程序的代码。

我使用 Visual Studio 2008 创建了 MiniCalc 应用程序。在启动 Visual Studio 后,单击“文件”|“新建”|“网站”。为了避免使用 ASP.NET 代码隐藏机制并将 Web 应用程序的所有代码保存在一个文件中,我选择了“空网站”选项。接下来,从“位置”字段下拉列表中选择“HTTP 模式”选项(而非“文件模式”),并将位置指定为:

http://localhost/TestWithJQuery/MiniCalc

我决定对 MiniCalc 应用程序逻辑使用 C#。 此处介绍的测试自动化技术可与以 C# 和 Visual Basic 编写的 ASP.NET Web 应用程序以及通过传统的 ASP、CGI、PHP、JSP、Ruby 等技术创建的 Web 应用程序一起使用。

在“新建网站”对话框上单击“确定”,以配置 IIS 并生成 Web 应用程序的结构。 接下来,转到解决方案资源管理器窗口,右键单击 MiniCalc 项目名称,并从上下文菜单中选择“添加新项”。 然后,从已安装的模板列表中选择“Web 窗体”,并接受 Default.aspx 文件名。 我清除了“将代码放在单独的文件中”选项,然后单击“添加”按钮。

接下来,在解决方案资源管理器中双击 Default.aspx 文件名,以将模板生成的代码加载到文本编辑器。 我删除了所有模板代码并替换为图 2 中所示的代码。

图 2 待测试的 MiniCalc Web 应用程序的源

<%@ Page Language="C#" %>
<script runat="server">
  static Random rand = null;
  private void Page_Load(object sender, EventArgs e)
    if (!IsPostBack) 
      rand = new Random(0);
  private void Button1_Click(object sender, System.EventArgs e)
    int randDelay = rand.Next(1, 6); // [1-5]
    System.Threading.Thread.Sleep(randDelay * 1000);
    int x = int.Parse(TextBox1.Text);
    int y = int.Parse(TextBox2.Text);
    if (RadioButton1.Checked)
      TextBox3.Text = (x + y).ToString("F4");
    else if (RadioButton2.Checked)
      TextBox3.Text = (x * y).ToString("F4");
</script>
  (client-side JavaScript and UI elements here)
</html>

为确保应用程序代码尽可能小并且易于理解,我省略了常规错误检查。 可以从 code.msdn.microsoft.com/mag201012TestRun 处获取 MiniCalc 应用程序的完整源代码和测试工具。

若要为 Web 应用程序编写测试自动化,您通常必须知道各用户控件的 ID。 如图 2 所示,我使用 TextBox1 和 TextBox2 来保留两个用户整数输入值(RadioButton1 和 RadioButton2)以选择加法或乘法,并使用 TextBox3 来保留算术计算结果。

用户单击 Button1 控件时,MiniCalc 应用程序首先会进入 1 至 5 秒的随机延迟,以模拟某种服务器端处理,然后计算并显示两个用户输入值的总和或乘积。

接下来,我决定使用 AJAX 技术让 MiniCalc 应用程序实现异步。 要完成此任务,我需要为应用程序创建一个 web.config 文件,而无需从头手动创建一个 web.config 文件。我按下 F5 键以指示 Visual Studio 通过调试器生成并运行应用程序。 当 Visual Studio 提示我允许添加 web.config 文件时,我单击了“确定”。 接下来,我向 MiniCalc 应用程序中添加了一个 ScriptManager 服务器端控件,以启用 AJAX:

<asp:ScriptManager ID="sm1" runat="server" EnablePartialRendering="true" />

然后,我添加了异步更新与 Button1 单击事件结合的 TextBox3 结果元素所必需的标记:

<asp:UpdatePanel ID="up1" runat="server">
<ContentTemplate>
<p><asp:TextBox id="TextBox3" width="120"  runat="server" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1" EventName="Click" />
</Triggers>
</asp:UpdatePanel>

如果仔细检查图 1,您将会发现,为了强调 MiniCalc 是 AJAX 应用程序的事实,我在 UI 中放置了一个客户端页面生命计数器。 当对 MiniCalc 的异步请求返回时,只会更新 TextBox3,而不会重置页面生命计数器。 将 pageLife 文本框定义为:

<input type="text" id="pageLife" size="1"/>

相关的客户端 JavaScript 为:

<script language="javascript">
  var count = 0;
  function updatePageLife() {
    ++count;
    var tb = document.getElementById("pageLife");
    tb.value = parseInt(count);
    window.setTimeout(updatePageLife, 1000);
</script>

计数器由应用程序 onload 事件启动:

<body bgColor="#ccffff" onload="updatePageLife();">

使用 jQuery 进行 Web 应用程序 UI 测试

现在,您已了解了待测试的 Web 应用程序,让我们直接开始探讨 UI 测试自动化代码。 主要的测试工具只是带有两个 frame 元素的普通 HTML 页面:

<!-- UITestHarness.html --> <title>Test Harness for MiniCalc AJAX Web App</title> </head> <frameset cols="45%,*" onload="leftFrame.appLoaded=true"> <frame src="http://localhost/TestWithJQuery/TestScenario001.html" name="leftFrame" > <frame src="http://localhost/TestWithJQuery/MiniCalc/Default.aspx" name="rightFrame"> </frameset> </html>

名为 rightFrame 的框架按原样承载待测试的 Web 应用程序,该应用程序从未进行过任何修改或测试检测。 名为 leftFrame 的框架承载名为 TestScenario001.html 的 HTML 页面,该页面包含所有 jQuery 测试自动化代码。 请注意,当触发 frameset 元素 onload 事件时,leftFrame 页中名为 appLoaded 的变量将设置为 true。 此变量将用于确保在待测试的 Web 应用程序完全加载到测试工具之后才开始进行测试自动化。 测试方案代码的结构如图 3 所示。

图 3 UI 测试自动化页的结构

<!-- TestScenario001.html --> <script src='http://localhost/TestWithJQuery/jquery-1.3.2.js'></script> <script type="text/javascript"> $(document).ready(function() { logRemark("jQuery Library found and harness DOM is ready\n"); var testScenarioID = "Test Scenario 001"; var maxTries = 20; var numTries; var polling = 500; // milliseconds var appLoaded = false; var started = false; function launch() { if (!started) runTest(); function waitUntilAppLoaded() { // Code function runTest() { // Start automation function step1() { // Manipulate state function clickCalculate() { // Click the Calculate button function checkControl(controlID, controlVal) { // Determine if control has specified value function step2() { // Manipulate state function callAndWait(action, checkControlFunc, controlID, controlVal, callbackFunc, pollTime) { // The heart of the automation function doWait(checkControlFunc, controlID, controlVal, callbackFunc, pollTime) { // Wait until Web app responds function finish() { // Determine pass/fail result function logRemark(comment) { // Utility logging function </script> </head> <body bgcolor="#F5DEB3"> <h3>This is the UI test scenario with jQuery script page</h3> <p>Actions:</p><p><textarea id="comments" rows="22" cols="34"> </textarea></p> <input type="button" value="Run Test" onclick="runTest();" /> </body> </html>

测试脚本首先会引用 jQuery 库:

<script src='http://localhost/TestWithJQuery/jquery-1.3.2.js'>

这里所指向的是已从 jQuery 项目网站 (jquery.com) 中下载并复制到 MiniCalc 应用程序根目录的 jQuery 库的本地副本。 我使用了 jQuery 版本 1.3.2。 我们一直在不断地开发该库,所以您在阅读本文的时候可能存在较新的版本。 有关在您的代码中引用 jQuery 库的详细信息,请参阅“获取 jQuery 库”。

获取 jQuery 库

对于您的应用程序所使用的 jQuery 库的位置,您可以有多个选择。 如前文所述,您可从 jquery.com 中下载最新版本并从您的本地文件系统中使用它。 jQuery 网站提供了开发(未压缩)和生产(已缩小且已删除空格,以占用较少的空间)下载。 只需选择您想要的包,并将 .js 文件保存到您的项目目录即可。

如果您的应用程序主机有活动的 Internet 连接,则还有更简单的方法,那就是指向在线内容传送网络 (CDN) 提供的 jQuery 的最新版本。 您有大量资源(包括您自己的托管版本)可以使用,但有两个 CDN 是高度可用的,即 Microsoft AJAX 内容传送网络 (asp.net/ajaxlibrary/cdn.ashx) 和 Google Libraries API (code.google.com/apis/libraries)。

例如,您可使用 Microsoft Ajax CDN 中带有以下脚本标记的缩小版 jQuery:

<script 
  src="https://ajax.microsoft.com/ajax/jquery/jquery-1.3.2.min.js" 
  type="text/javascript">
</script>

Scott Guthrie 发表过一篇很有用的、关于对 jQuery 和 ASP.NET AJAX 使用 Microsoft Ajax CDN 的博客文章,网址为 tinyurl.com/q7rf4w

通常,将 jQuery 用于测试自动化时,使用测试工具中的库的本地未打包副本比使用远程或已打包副本更可靠。 但对于生产应用程序,您将希望使用一个可靠的托管库。

接下来,我使用一个标准的 jQuery 惯例来确定自动化是否具有访问 jQuery 库的权限:

$(document).ready(function() {
  logRemark("jQuery Library found and harness DOM is ready\n");

当包含文档 DOM 完全加载到测试主机内存,并且所有的 DOM 元素都可用后就会立即触发 jQuery ready 函数。 如果 jQuery 库无法访问(指定了不正确的库路径时通常会发生这种情况),则会引发“缺少对象”错误。

ready 函数会将匿名函数当作其单个参数接受。 匿名函数会频繁地用于对 jQuery 和 JavaScript 的测试自动化中。 您可将匿名函数当作使用函数关键字动态定义的函数。

下面是一个名为 logRemark 的函数的示例:

function logRemark(comment) {
  var currComment = $("#comments").val();
  var newComment = currComment + "\n" + comment;
  $("#comments").val(newComment);

这种情况下,我定义了一个函数,该函数只调用一个名为 logRemark 的程序定义的日志记录函数来对 jQuery 可用的测试工具显示消息。 同时,我还使用了内部 JavaScript 警告函数。

首先,我使用 jQuery 选择器和链接语法获取 ID 为“comments”的文本区中的当前文本。记号 $ 是 jQuery 元类的快捷别名。 # 语法用于按照 ID 选择 HTML 元素,而 val 函数可充当值 setter 和 getter(面向对象的编程术语中的属性)。 我在现有注释文本中附加了一个 comment 参数和换行字符,然后使用 jQuery 语法更新 TextArea 元素。

接下来,我设置了一些测试自动化全局变量:

var testScenarioID = "Test Scenario 001";
var maxTries = 20;
var numTries;
var polling = 500;
var appLoaded = false;
var started = false;

因为我的自动化处理的是异步应用程序,所以不使用任意时间延迟。 相反,我使用一系列短暂(由变量轮询定义)延迟,再由变量 numTries 进行反复检查,以查看某些 HTML 元素的值是否满足 Boolean 条件,检查次数最多达到变量 maxTries 的最大尝试次数。 在本测试方案中,我在总共十秒的时间内最多对 20 次尝试使用了延迟,每次尝试的延迟时间为 500 毫秒。 appLoaded 变量用于确定待测试的 Web 应用程序完全加载到测试工具的时间。 started 变量用于协调测试工具的执行。

若要手动启动自动化,您可单击“运行测试”按钮:

<input type="button" value="Run Test" onclick="runTest();" />

图 3 中所示的 launch 函数用于完全测试自动化,稍后我会进行解释。 runTest 函数将充当测试自动化的主要协调函数:

function runTest() {
  waitUntilAppLoaded();
  started = true;
  try {
    logRemark(testScenarioID);
    logRemark("Testing 3 + 5 = 8.0000\n");
    step1();
  catch(ex) {
    logRemark("Fatal error: " + ex);

runTest 函数首先会调用 waitUntilAppLoaded 函数,该函数的定义如下所示:

function waitUntilAppLoaded() {
  if (appLoaded == true) return true;
  else window.setTimeout(waitUntilAppLoaded, 100);

请记住,测试方案会将变量 appLoaded 初始化为 false,工具框架集 onload 事件会将 appLoaded 设置为 true。 此处,我使用内部 setTimeout 函数来重复暂停 100 毫秒,直到 appLoaded 的值变为 true。 请注意,此方法可能造成永久性延迟。 若要避免这种可能,您可能希望在最大延迟次数之后添加一个全局计数器并返回 false。

设置全局 start 变量后,runTest 将在异常处理程序包装中显示某些注释并调用 step1 函数。 我在此处介绍的工具结构只有一种,您可根据您的编程风格和测试环境来修改工具组织。 在我的结构中,我会将测试方案当作一系列状态更改,每次更改由 stepX 函数表示。

step1 函数通过模拟用户输入来处理待测试的 Web 应用程序的状态,如图 4 所示。

图 4 使用 step1 函数进行模拟输入

function step1() {
  logRemark(
    "Entering 3 and 5 and selecting Addition");
  var tb1 = 
    $(parent.rightFrame.document).find('#TextBox1');
  tb1.val('3');
  var tb2 = 
    $(parent.rightFrame.document).find('#TextBox2');
  tb2.val('5');
  var rb1 = 
    $(parent.rightFrame.document).find('#RadioButton1');
  rb1.attr("checked", true);
  logRemark(
    "\nClicking Calculate, waiting for async response '8.0000'");
  callAndWait(clickCalculate, checkControl, "TextBox3", "8.0000", 
    step2, polling);

用于访问和处理 HTML 元素的 jQuery 语法具备一致性和明确性,并且大部分都是独立于浏览器的。 请注意,若要通过 leftFrame 元素中的代码访问 rightFrame 元素中加载的 Web 应用程序,则必须使用 parent 关键字。 还请注意,必须使用 jQuery 查找筛选器。

操作 TextBox1 和 TextBox2 元素时,我假定已将待测试的 Web 应用程序完全加载到了 rightFrame 元素中。 对于加载时间较长的应用程序,此假设可能不合理,在这种情况下,您可将 jQuery 选择器代码放置到 window.setTimeout 延迟循环中,以便对内置的“未定义”值测试目标对象。

由于待测试的 MiniCalc 应用程序是一个 AJAX 应用程序,所以我的工具不能只调用“Calculate”(计算)按钮单击事件,这是因为测试工具代码将不会等待应用程序的异步响应,而是继续执行。 因此,我使用了一个程序定义的 callAndWait 函数:

function callAndWait(action, checkControlFunc, controlID,
  controlVal, callbackFunc, pollTime) {
    numTries = 0;
    action();
    window.setTimeout(function(){doWait(
      checkControlFunc, controlID, controlVal, 
      callbackFunc, pollTime);}, pollTime);

callAndWait 函数将调用一个函数(action 参数),进入延迟循环,并暂停一段很短的时间(变量 pollTime),然后通过调用带有形参 controlID 和 controlVal 的参数函数 checkControlFunc 来检查某些应用程序的状态是否为 true。 当 checkControlFunc 返回 true,或者已执行延迟的最大次数时,系统会将控制权转交给参数函数 callbackFunc。

callAndWait 函数将与程序定义的 doWait 函数结合使用:

function doWait(checkControlFunc, controlID, 
  controlVal, callbackFunc, pollTime) {
  ++numTries;
  if (numTries > maxTries) finish();
  else  if (checkControlFunc(controlID, controlVal)) 
    callbackFunc();
  else window.setTimeout(function(){
    doWait(checkControlFunc, controlID,
    controlVal, callbackFunc, pollTime);}, pollTime);

当 checkControlFunc 返回 true 或本地计数器 numTries 超出全局变量 maxTries 时,doWait 函数将出现递归并将退出。 因此,这将调用一个名为 clickCalculate 的函数,进入延迟循环,暂停轮询 500 毫秒并调用带有参数 TextBox3 和 8.0000 的函数 checkControl,直到 checkControl 返回 true 或延迟循环执行 20 次(由 maxTries 指定):

callAndWait(clickCalculate, checkControl, "TextBox3", 
  "8.0000", step2, polling);

如果 checkControl 返回 true,则会将控制权转交给函数 step2。 clickCalulate 函数使用 jQuery 选择和链接:

function clickCalculate() {
  var btn1 = $(parent.rightFrame.document).find('#Button1');
  if (btn1 == null || btn1.val() == undefined) 
    throw "Did not find btn1";
  btn1.click();

按照此方法定义操作包装函数的主要原因是便于将该函数按名称传递到 callAndWait 函数。 checkControl 函数很简单:

function checkControl(controlID, controlVal) {
  var ctrl = $(parent.rightFrame.document).find('#' + controlID);
  if (ctrl == null || ctrl.val() == undefined || ctrl.val() == "")
    return false;
    return (ctrl.val() == controlVal);

首先,我使用 jQuery 语法来获取对参数 controlID 所指定的控件的引用。 如果该控件的值尚不可用,则立即返回到延迟循环。 一旦该控件值准备就绪,就可以检查其是否与参数 controlVal 提供的某些预期值相等。

在调用我想调用的数量的 stepX 函数之后,我会将控制权转交给 finish 函数。 该函数首先会确定它是如何被调用的:

if (numTries > maxTries) {
  logRemark("\nnumTries has exceeded maxTries");
  logRemark("\n*FAIL*");
else ....

如果全局 numTries 变量的值超过 maxTries 的值,则我就知道待测试的 Web 应用程序在允许的时间内未作出响应。 这里,我断定这是测试用例失败,而不是某种形式的不确定结果。 如果 numTries 尚未超过 maxTries,则我会开始检查待测试的应用程序的最终状态:

logRemark("\nChecking final state");
var tb1 = $(parent.rightFrame.document).find('#TextBox1');
var tb2 = $(parent.rightFrame.document).find('#TextBox2');
var tb3 = $(parent.rightFrame.document).find('#TextBox3');

此处,我获得了对三个文本框控件的引用。 事实上,您决定要检查的待测试的 Web 应用程序的元素将具体取决于特定应用程序的详细信息。 接下来,我会检查每个文本框控件的值,以查看是否每个控件都得到了预期值:

var result = "pass";
if (tb1.val() != "3") result = "fail";
if (tb2.val() != "5") result = "fail";
if (tb3.val() != "8.0000") result = "fail";

我的测试方案脚本硬编码了所有测试案例输入和预期值。 我介绍的测试自动化最适合轻型和快速测试情形,其中的硬编码测试数据简单而有效。

finish 函数通过显示“通过”或“未通过”结果来总结测试运行:

if (result == 'pass')
  logRemark("\n*Pass*");
  logRemark("\n*FAIL*");

与测试用例输入数据一样,此方法十分轻便,您可能想要在测试主机或 Web 服务器上将测试结果写入到外部文件,或者通过 SMTP 将测试结果发送到电子邮件地址。

此处所介绍的工具为半自动化工具,因为您必须单击按钮控件才能启用测试。 您可通过添加一个 start-wrapper 函数使该工具完全实现自动化:

function launch() {
  if (!started)
    runTest();

向工具页中的框架集元素中添加属性 onload=“leftFrame.launch();”。 工具中每次 Web 应用程序的加载都将触发一个 onload 事件,因此我使用全局“start”变量来阻止测试自动化重新启动。 有趣的是,即使 HTML Frame 元素不支持 onload 事件,您实际上仍可在工具框架元素中放置一个 onload 属性,该事件将会从其父框架集元素中冒出。

现在,您可使用以下命令创建 .bat 文件:

iexplore http://localhost/TestWithJQuery/UITestHarness001.html
iexplore http://localhost/TestWithJQuery/UITestHarness002.html

执行 .bat 文件后(可能通过 Windows 任务计划程序执行),工具将会加载,您的自动化将会自动启动。 另一种扩展我在此处介绍的测试系统的方法是将程序定义的函数放置到 jQuery 插件中。

在编写轻型 Web 应用程序 UI 测试自动化时,对于我在此处介绍的基于 jQuery 的方法,您可有多种选择。 与使用原始 JavaScript 相比,使用 jQuery 库的主要优点就是 jQuery 可跨多台浏览器(如 Internet Explorer、Firefox 和 Safari)工作。 另一个显著的优点就是,通过使用 jQuery 编写测试自动化,您可主动构建对 Web 开发任务使用 jQuery 这方面的知识。

与其他方法相比,使用 jQuery 确实存在劣势。 使用 jQuery 在某种程度上需要一个外部依赖项,并且与非脚本测试自动化相比,基于脚本的测试自动化在管理上往往更加困难。 与使用 Selenium 或 Watir 等测试框架相比,编写基于 jQuery 的自动化可提供更大的灵活性,但您必须在更低的抽象级别上编写代码。

与往常一样,我要提醒您,没有任何一种特定的测试自动化方法能适用于所有情况,但是,在许多软件开发方案中,基于 jQuery 的 Web 应用程序 UI 测试自动化可能是有效和高效的技术。

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

衷心感谢以下技术专家对本文的审阅: Scott Hanselman 和 Matthew Osborn