使用 JavaScript 实现 Web 应用程序请求-响应测试
James McCaffrey McCaffrey
在本月专栏中,我将介绍如何使用 JavaScript 编写简单、有效的基于浏览器的请求-响应测试自动化程序。要想了解我所讲述的内容,最好是看一下图 1 和 2 所示的屏幕快照。图 1 是要进行“产品搜索”测试的 ASP.NET Web 应用程序,它简单但具有代表性。用户在此应用程序的单个文本框控件中输入一些搜索字符串,并使用两个单选按钮控件指定执行搜索时是否要区分大小写。搜索结果将在列表框控件中显示。
图 1 要测试的“产品搜索”Web 应用程序
虽然要测试的示例 Web 应用程序基于 ASP.NET,但我在本文中介绍的技术也可用于为 Web 应用程序创建测试自动化程序,而这些应用程序是使用最具动态性的页面生成技术(包括 PHP、JSP、CGI 以及其他)编写而成。
图 2 显示的是正在运行的请求-响应测试自动化程序。请注意,测试自动化程序工具是基于浏览器的。与其他可选方法比较,此处介绍的技术的优势之一就是,它可与最主要的 Web 浏览器一起使用,并且可在运行大多数操作系统的测试主机上执行。
图 2 请求-响应测试运行
该测试自动化程序工具由单个 HTML 页面构成,该页面具有相对简短的一组 JavaScript 函数。请注意,测试运行输出的第一行指示测试自动化程序正在使用 jQuery 库。此工具将读取对应于用户输入的测试用例输入数据,然后以编程方式向“产品搜索”Web 应用程序提供该输入数据。该工具接受生成的 HTTP 响应数据并检查该响应是否为预期值,从而确定测试用例结果为通过还是失败。
在本文接下来的部分中,我将首先简要介绍图 1 中所示的要测试的 Web 应用程序,从而方便您了解哪些因素与 HTTP 请求-响应测试有关。然后,将详细解释在图 2 中运行的测试工具代码,这样您就能够为满足自身需求对该工具进行修改。最后,将简要总结何时适合使用 JavaScript 实现基于浏览器的请求-响应测试自动化程序,以及何时更加适合使用其他可选方法。
在本文中,假设您的 JavaScript 和 ASP.NET 技能为中级水平,但是即使您初学这些技术,也应该能理解我的讲解。
构建 Web 应用程序
我使用 Visual Studio 2008 创建要测试的“产品搜索”Web 应用程序。为了利用 Visual Studio 配置网站的功能,我在主菜单栏上选择了“文件”|“新建”|“网站”。然后,在生成的“新建网站”对话框中选择“空网站”选项。为创建完整的 ASP.NET 网站,我在本地计算机上指定了 HTTP 位置,而不是为使用内置 Visual Studio 开发服务器指定文件系统位置。我选择 C# 语言作为逻辑代码。
单击“确定”后,Visual Studio 就创建了空的“产品搜索”网站。在“解决方案资源管理器”窗口中,右键单击“产品搜索”项目,然后从上下文菜单中选择“添加新项”。我选择了“Web 窗体”项,接受了默认页面名称 Default.aspx 并单击“添加”,页面随即生成。然后,我为要测试的 Web 应用程序创建了简单的 UI,如图 3 中所示。
图 3 Web 应用程序 UI
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Product Search</title>
</head>
<body bgcolor="#ccbbcc">
<form id="form1" runat="server">
<div>
<asp:Label ID="Label1" runat="server" Text="Find:"
Font-Names="Arial" Font-Size="Small">
</asp:Label>
<asp:TextBox ID="TextBox1" runat="server" Width="114px">
</asp:TextBox>
<asp:Button ID="Button1" runat="server" onclick="Button1_Click"
Text="Go" />
<br />
<asp:RadioButtonList ID="RadioButtonList1" runat="server"
Font-Names="Arial" Font-Size="Small">
<asp:ListItem>Case Sensitive</asp:ListItem>
<asp:ListItem Selected="True">Not Case Sensitive</asp:ListItem>
</asp:RadioButtonList>
</div>
<asp:ListBox ID="ListBox1" runat="server" Height="131px" Width="246px"
Font-Names="Courier New" Font-Size="Small">
</asp:ListBox>
</form>
</body>
</html>
因为我将进行简要解释,所以,当创建 HTTP 请求-响应测试自动化程序时,您必须了解您希望模拟用户输入的任何输入控件的 ID。这样,我就能够访问要测试的应用程序的源代码,但是即使您没有源代码访问权限,也始终能够使用 Web 浏览器的查看源代码功能确定输入控件 ID。请注意,您或许认为这两个单选按钮控件由两个控件表示,但实际上是由一个 ID 为 RadioButtonList1 的输入控件表示。
我将此应用程序逻辑直接添加到 Defaut.aspx 文件,而没有使用代码隐藏机制。在页面的顶部,我创建了一个脚本块,用于放置应用程序的逻辑代码:
<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
// ...
</script>
我在该脚本块中添加了一个小类,用于代表 Product 对象:
public class Product {
public int id;
public string desc;
public Product(int id, string desc) {
this.id = id; this.desc = desc;
}
}
然后又添加了一个内部应用程序作用域 ArrayList 对象,用于模拟外部数据存储:
public static ArrayList data = null;
在大多数实际的 Web 应用程序方案中,数据存储通常是外部的,如 XML 文件或 SQL Server 数据库。但是,当执行 HTTP 请求-响应测试时,在某种程度上,应用程序的数据存储位置无关紧要。HTTP 请求并不知道数据存储的位置,并且 HTTP 响应通常只包含 HTML。然后,我又添加了一些代码,用来使用 Product 项填充内部数据存储:
protected void Page_Load(object sender, EventArgs e) {
if (!IsPostBack) {
data = new ArrayList();
Product p1 = new Product(111, "Widget");
Product p2 = new Product(222, "Gizzmo");
Product p3 = new Product(333, "Thingy");
data.Add(p1); data.Add(p2); data.Add(p3);
}
}
最后,我将所有应用程序逻辑放置到事件处理程序中,以在 Button1 单击事件时调用。首先,清空 ListBox1 结果区域并提取用户输入:
ListBox1.Items.Clear();
string filter = TextBox1.Text.Trim();
string sensitivity = RadioButtonList1.SelectedValue;
区分大小写字符串变量包含“区分大小写”或“不区分大小写”。
之后,我将标头信息放置到 ListBox1 结果区域、声明一个保存 Product 搜索结果的字符串并初始化一个计数器以跟踪与搜索筛选器匹配的 Product 项的个数:
ListBox1.Items.Add("ID Description");
ListBox1.Items.Add("================");
string resultRow;
int count = 0;
我在 ArrayList 数据存储检查中遍历每个 Product 对象,以查看搜索筛选器字符串是否与当前对象的说明字段匹配:
foreach (Product p in data) {
resultRow = "";
if (sensitivity == "Not Case Sensitive" &&
p.desc.IndexOf(filter,
StringComparison.CurrentCultureIgnoreCase) >= 0) {
resultRow = p.id + " " + p.desc; ++count;
}
else if (sensitivity == "Case Sensitive" &&
p.desc.IndexOf(filter) >= 0) {
resultRow = p.id + " " + p.desc; ++count;
}
if (resultRow != "") ListBox1.Items.Add(resultRow);
}
对于与搜索筛选器匹配的每个产品,我将构建一个结果字符串并增加计数器的值。请注意,IndexOf 方法适时进行重载以接受区分大小写参数。
应用程序逻辑最后会向 ListBox1 显示区域添加空白行和计数结果:
ListBox1.Items.Add("");
ListBox1.Items.Add("Found " + count + " matching items");
为使 Web 应用程序尽可能小并且使用简单,我使用了许多快捷方式,而这些快捷方式不会在生产环境中使用。尤其是,我并未提供任何错误检查或处理功能。
请求-响应测试自动化程序
我已使用记事本创建了测试工具页,如图 2 中运行的内容所示。此工具的整体结构如图 4 中所示。
图 4 测试工具结构
<html>
<!-- RequestResponseTests.html -->
<head>
<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 targetURL = ‘http://localhost/TestWithJQuery/ProductSearch/Default.aspx’;
var testCaseData =
[ ‘001,TextBox1=T&RadioButtonList1=Case+Sensitive&Button1=clicked,333 Thingy’,
‘002,TextBox1=t&RadioButtonList1=Not+Case+Sensitive&Button1=clicked,Found 2 matching items’ ];
function runTests() {
try {
logRemark(‘Begin Request-Response with JavaScript test run’);
logRemark("Testing Product Search ASP.NET application\n");
// ...
logRemark("\nEnd test run");
}
catch(ex) {
logRemark("Fatal error: " + ex);
}
}
function getVS(target) {
// ...
}
function getEV(target) {
// ...
}
function sendAndReceive(target, rawVS, rawEV, inputData) {
// ...
}
function logRemark(comment) {
// ...
}
</script>
</head>
<body bgcolor="#66ddcc">
<h3>Request-Response Test Harness Page</h3>
<p><b>Actions:</b></p><p>
<textarea id="comments" rows="24" cols=63">
</textarea></p>
<input type="button" value="Run Tests" onclick="runTests();" />
</body>
</html>
工具 UI 代码位于此页面底部的主体元素中,仅由一些文本、用于显示信息的 textarea 元素以及启动测试自动化程序的按钮构成。
该测试工具结构的第一步就是使用脚本元素 src 属性来引用 jQuery 库。jQuery 库是一个开放的 JavaScript 函数源集合,可从 jquery.com 获得。虽然您认为 jQuery 是随着 Web 开发而创建,但该库中包含的函数可使其非常适合轻型请求-响应测试自动化程序。这里所指向的是该库的 1.3.2 版本的本地副本。在进行测试自动化时,使用该库的本地副本比指向远程副本更加可靠。接下来,我使用 $(document).ready jQuery 表达式来确保我的工具可以访问该库,并且确保该工具 DOM 已加载到内存中。
在设置指向要测试的 Web 应用程序的变量 targetURL 后,我将内部逗号分隔的测试用例硬编码为字符串数组 testCaseData。在此,我仅硬编码了两个测试用例,但是在生产环境中,您可能会遇到上百个用例。通常,外部测试用例数据比内部测试用例数据更具优势,因为外部数据更易于修改和共享。但是,因为我在此介绍的技术属于轻型技术,所以内部测试用例数据是合理的设计选择。
测试用例中的第一个字段是用例 ID 号。第二个字段是发送到要测试的应用程序的原始请求数据。第三个字段是预期结果。
我如何知道请求数据的格式?确定 HTTP 请求数据格式的最简单方法,就是使用要测试的应用程序执行初步分析,通过使用 Fiddler 等 HTTP 记录程序检查实际的请求数据。
运行测试
主要的工具控制函数是 runTests。此 runTests 函数使用一流的 try-catch 机制进行初步错误处理。我使用辅助函数 logRemark 显示工具的 textarea 元素的信息。此工具使用帮助程序函数 getVS 和 getEV 获取要测试的 ASP.NET Web 应用程序的当前 ViewState 和 EventValidation 值。这些值由应用程序生成且由 Base64 编码,主要起到状态和安全机制的作用,并且必须作为任何 HTTP POST 请求的一部分进行发送。sendAndReceive 函数执行实际的 HTTP 请求并返回相应的 HTTP 响应。runTests 函数迭代每个测试用例:
for (i = 0; i < testCaseData.length; ++i) {
logRemark("==========================");
var tokens = testCaseData[i].split(‘,’);
var caseID = tokens[0];
var inputData = tokens[1];
var expected = tokens[2];
...
我使用内置 Split 函数将每个测试用例字符串分为更小的片段。然后,再调用 getVS 和 getEV 帮助程序函数:
logRemark(‘Case ID : ‘ + caseID);
logRemark(‘Fetching ViewState and EventValidation’);
var rawVS = getVS(targetURL);
var rawEV = getEV(targetURL);
此主处理循环通过调用 sendAndReceive 函数并检查生成的 HTTP 响应,继续查找关联测试用例预期的值:
var response = sendAndReceive(targetURL, rawVS, rawEV, inputData);
logRemark("Expected : ‘" + expected + "’");
if (response.indexOf(expected) >= 0)
logRemark("Test result : **Pass**");
else if (response.indexOf(expected) == -1)
logRemark("Test result : **FAIL**");
} // main loop
getVS 帮助程序函数依赖 jQuery 库:
function getVS(target) {
$.ajax({
async: false, type: "GET", url: target,
success: function(resp) {
if (resp.hasOwnProperty("d")) s = resp.d;
else s = resp;
start = s.indexOf(‘id="__VIEWSTATE"’, 0) + 24;
end = s.indexOf(‘"’, start);
}
});
return s.substring(start, end);
}
getVS 函数的主要设想就是,向要测试的应用程序发送基本的 GET 请求、提取响应并解析出 ViewState 值。$.ajax 函数可接受匿名函数。Async、type 和 URL 参数应该都可以对自身进行说明。响应 resp 对象的 hasOwnProperty(“d”) 方法主要是一个在 Microsoft .NET Framework 3.5 中提供的安全机制,在这种情况下没有必要使用。
我通过查找该属性的起始位置,然后数过 24 个字符到达 ViewState 值真正开始的位置来提取 ViewState 值。getEV 函数代码与 getVS 代码完全相同,只是 EventValidation 值从初始 id=EVENTVALIDATION 属性的 30 个字符处开始。拥有单独的 getVS 和 getEV 函数后,您就可以灵活把握但需要两个单独的基本请求。另一种方法就是将 getVS 和 getEV 重构到单一的帮助程序函数。
sendAndReceive 帮助程序函数将执行实际的 HTTP 请求并提取生成的响应。此函数首先将原始 ViewState 和 EventValidation 字符串转换为 URL 编码的字符串,然后构建发布到 Web 应用程序的数据:
function sendAndReceive(target, rawVS, rawEV, inputData) {
vs = encodeURIComponent(rawVS);
ev = encodeURIComponent(rawEV);
postData = inputData + ‘&__VIEWSTATE=’ + vs +
‘&__EVENTVALIDATION=’ + ev;
...
对于发布数据中为非法值的字符,内置 encodeURIComponent 函数将它们编码为转义序列。例如,“/”字符编码为 %2F。在记录消息后,sendAndReceive 将使用 $.ajax 方法创建 HTTP POST 请求:
logRemark("Posting " + inputData);
$.ajax({
async: false,
type: "POST",
url: target,
contentType: "application/x-www-form-urlencoded",
data: postData,
...
之所以创建 $.ajax 方法,主要用于发送异步 XML HTTP 请求,但是将 async 参数设置为 false,该方法也可用于发送标准的同步请求。太巧妙了!您可将内容类型参数值看作奇妙的字符串,只是代表从 HTML 表单元素发布的数据。在获取关联的 HTTP 响应时,sendAndReceive 函数使用与 getVS 相同的模式:
success: function(resp, status) {
if (resp.hasOwnProperty("d")) s = resp.d;
else s = resp;
},
error: function(xhr, status, errObj) {
alert(xhr.responseText);
}
});
return s;
}
我还使用可选错误参数在警告框中显示任何致命错误。
关于此测试工具,最后要介绍的功能是 logRemark 实用程序:
function logRemark(comment) {
var currComment = $("#comments").val();
var newComment = currComment + "\n" + comment;
$("#comments").val(newComment);
}
我使用 jQuery 选择器和链接语法获取 textarea 元素中的当前文本,其中具有评论 ID。“#”语法用于按照 ID 选择 HTML 元素,并且 val 函数的作用等同于值 setter 和 getter。我在现有注释文本中附加了一个 comment 参数值和换行字符,然后使用 jQuery 语法更新 textarea 元素。
替代方案
对于基于浏览器的 JavaScript 语言方法的主要替代方案,我已在本文中进行了介绍,就是使用 C# 等语言创建基于 shell 的工具。与基于 shell 的方法比较,基于浏览器的方法最适合于高动态的环境,因为测试自动化程序的生命期很短。此外,此处介绍的基于浏览器的方法独立于平台。此项技术将适用于任何支持 jQuery 库和 JavaScript 的浏览器和 OS 组合。
James McCaffrey 博士 供职于 Volt Information Sciences Inc.,在该公司他负责管理面向在 Microsoft 工作的软件工程师的技术培训。他参与过多项 Microsoft 产品的研发工作,其中包括 Internet Explorer 和 MSN Search。McCaffrey 博士是《.NET 软件测试自动化之道》(Apress, 2006) 一书的作者。您可以通过电子邮件 jmccaffrey@volt.com 或 v-jammc@microsoft.com 与他联系。