使用 Visual Studio 2008 Web Test 轻松进行 ASP.NET AJAX 测试





5.00/5 (15投票s)
一套 ExtractionRules、ValidationRules 和 Request Plugin,让 ASP.NET 和 AJAX 网站测试变得轻而易举。无需录制测试,无需编写使用服务器端控件名称的参数化测试,无需处理 UpdatePanels,无需模拟点击按钮——所有这些都可以通过 Web Test 完成。
引言
Visual Studio 2008 提供了丰富的 Web 测试支持,但对于测试高度动态的 AJAX 网站来说,它还不够强大。在这些网站中,页面内容动态地从数据库生成,并且同一个页面的输出会根据某些外部数据源(例如 RSS feed)频繁地发生变化。尽管您可以使用 Web Test Record 功能通过运行真实的浏览器来录制一些浏览器操作,然后回放它们。但是,如果测试的页面每次访问时都发生变化,那么您录制的测试将不再按预期工作。录制的 Web Test 的问题在于它会存储生成的 ASP.NET 控件 ID、表单字段名称。如果页面不再生成相同的 ASP.NET 控件 ID 或相同的表单字段,那么录制的测试将不再工作。一个非常简单的例子是,在 VS Web Test 中,您可以说“点击 ID 为 `ctrl00_UpdatePanel003_SubmitButton002` 的按钮”,但您不能说“点击第三个 `UpdatePanel` 中的第二个提交按钮”。另一个关键限制是,在 Web Tests 中,您不能使用服务器端控件 ID(如“`SubmitButton`”)来寻址控件。您必须始终使用生成的客户端 ID,它通常是这样的怪异格式:“`ctrl_00_SomeControl001_SubmitButton`”。此外,如果您进行 AJAX 调用,其中某些调用会返回 JSON 或更新某个 `UpdatePanel`,然后根据服务器返回的响应,您想进行进一步的 AJAX 调用或发布刷新后的 `UpdatePanel`,那么录制的测试将无法正常工作。您*确实*可以选择手动编写测试代码来处理这种情况,但这对于使用 `UpdatePanel` 的手动编写测试来说相当困难,因为您必须跨异步回发跟踪页面视图状态、表单隐藏变量等。因此,我构建了一个库,使测试动态 AJAX 网站和富含 `UpdatePanel` 的 Web 页面变得更加容易。库中有几个 `ExtractionRule` 和 `ValidationRule`,它们可以轻松地测试 Cookie、响应头、JSON 输出,发现页面中的所有 `UpdatePanel`,在响应正文中查找控件,以及查找 `UpdatePanel` 中的控件。
这个 Web Test 框架有哪些功能?
首先,让我举一个可以使用此库进行测试的示例。我的开源项目 Dropthings 是一个 Web 2.0 启动页面,该页面由小部件组成。

每个小部件都由两个 `UpdatePanel` 组成。每个小部件都有一个标题区域(一个 `UpdatePanel`)和一个主体区域(另一个 `UpdatePanel`)。每个小部件都通过小部件行的唯一 ID(一个 `INT IDENTITY`)从数据库渲染。每个页面都有唯一的小部件,具有唯一的 ASP.NET 控件 ID。因此,您无法录制测试然后回放它,因为在不同访问中,同一页面的 ASP.NET 控件 ID 永远不会相同。这就是我的库派上用场的地方。
查看我做的 Web 测试

此测试模拟了一个匿名用户首次访问和在页面上进行一些活动。当匿名用户第一次访问 Dropthings 时,会创建两个页面并添加一些默认小部件。您还可以添加新小部件,拖放小部件,删除小部件。
此 Web Test 模拟了以下行为
- 访问主页
- 显示小部件列表,这是一个 `UpdatePanel`。它检查 `UpdatePanel` 是否包含 BBC World 小部件。
- 然后它点击“每日教程”小部件上的“编辑”链接,该链接会在 `UpdatePanel` 中动态显示一些选项。然后它尝试将 `UpdatePanel` 内的下拉列表值更改为 10。
- 从“小部件列表”中添加新小部件。确保 `UpdatePanel` 回发成功并渲染了新小部件。
- 删除新添加的小部件并确保该小部件已消失。
- 注销用户。
让我们一步一步构建 Web Test。首先,添加一个 Web Test 插件

这个 `ASPNETWebTestPlugin` 是随库提供的。它为您做了很多工作。我稍后会解释它。目前,请记住它将为您处理所有回发、视图状态和 `UpdatePanel` 问题。
然后添加一个请求来访问某个页面。这里我正在访问 _Default.aspx_。

如果您是 Visual Studio Web Tests 的新手,`{{Config.TestParameters.ServerURL}}` 是一个配置键,用于设置网站的 URL,例如 https://:8000. 配置文件是一个 XML 文件,定义如下
<TestParameters>
<ServerURL>https://</ServerURL>
<AnonCookieName>.DBANON</AnonCookieName>
<AuthCookieName>.DBAUTH12</AuthCookieName>
<SessionCookieName>ASP.NET_SessionId</SessionCookieName>
</TestParameters>
一旦我访问页面,我会确保以下几点
- 使用 `CookieValidationRule` 生成持久性匿名 cookie。它检查 `.DBANON` cookie 是否由 ASP.NET `AnonymousIdentficationProvider` 生成。它还确保没有生成身份验证 cookie,例如 `.DBAUTH12` cookie。这些是 ASP.NET Membership cookie。我在这里测试了肯定和否定场景。肯定场景是我们期望的——生成匿名 cookie。否定场景是我们不期望的——不应该有某些用户的身份验证 cookie。第一个 Rule 在 Properties 窗口中的配置如下
- 确保没有生成缓存头,这会错误地在浏览器中缓存页面。我们不希望页面在任何地方被缓存。`CacheHeaderValidation` 类执行此检查。
- 然后我使用一对 `FindText` 验证规则来确保生成的页面具有默认小部件,方法是查找“每日教程”、“保留所有权利”等文本。一个好的做法是测试页眉、主体和页脚区域的一些文本,以确保整个页面内容至少已正确传递。
现在我将点击一个链接,该链接将打开一个 `UpdatePanel` 并检查 `UpdatePanel` 中的某些内容。以下测试点击 Dropthings 上看到的“添加内容”链接,该链接会在服务器动态渲染的 `UpdatePanel` 中显示一个小部件列表。
这里令人兴奋的是 `AsyncPostbackRequestPlugin`。这个请求插件专门用于处理 `UpdatePanel` 内发生的异步回发。它只有两个您需要定义的属性:需要点击或回发的控件的名称,以及包含该控件的 `UpdatePanel`。
这部分非常神奇——控件的名称是服务器端名称,也就是您在 _.aspx_ 或 _.ascx_ 文件中使用的名称。您无需指定客户端 ID,它可能以怪异的格式出现,如 `ctrl00_MasterPage_ContentHandler001_ShowAddContentPanel`。类似地,您可以使用 `UpdatePanel` 的服务器端名称来寻址 `UpdatePanel`。前缀 `$UPDATEPANEL` 用于标识 `UpdatePanel`,后跟服务器端名称,然后是索引号或第 n 个 `UpdatePanel` 号。这里我使用的是“`.1`”,因为我想命中第一个具有服务器端 ID `OnPageMenuUpdatePanel` 的 `UpdatePanel`。
此步骤点击名为“`OnPageMenuUpdatePanel`”的 `UpdatePanel` 中名为“`ShowAddContentPanel”的控件。
现在我们将进行一些复杂的表单回发。我们想点击一个链接,该链接将在 `UpdatePanel` 中生成一些新控件。然后我们将修改这些控件,点击一个保存控件并使用新值刷新 `UpdatePanel` 的按钮。

如果您没有打开 Dropthings,请打开它。然后点击“每日教程”小部件上的“编辑”链接。您会注意到它显示一个下拉列表,用于定义要显示多少条 RSS feed。默认设置为 5
。我将将其更改为 10
并保存设置,然后查看小部件是否显示 10
条 feed 链接。

第一步是点击“编辑”链接,它会刷新 `UpdatePanel` 以显示 Feed 数量下拉列表。
这是第一个 `AsyncPostbackRequestPlugin` 属性

由于“每日教程”小部件是页面上的第一个小部件,我在这里 everywhere 使用“`.1`”。如果要测试第二个小部件,请将其更改为“`.2`”
现在,第二个请求将下拉列表的值设置为 10
,然后点击“关闭编辑”链接以应用更改。点击后,它将刷新 feed 链接并显示 10 个链接。我用来验证是否真的在 `UpdatePanel` 中生成了 10 个链接的方法是使用 `FindText ValidationRule`。

我在这里检查 `FeedLink_ctl09_FeedLink` 链接是否存在。如果存在,则表示已生成 10 个链接。关键是检查 `ctl09`。
Web Test 中的其余步骤遵循相同的原理。它们点击某些内容,期望某些输出,使用输出将某些内容回发到服务器,然后检查回发是否成功。
代码演练
是时候带您了解幕后工作的类,它们为您带来了简化的 Web Test 框架。首先是 `ASPNETWebTestPlugin`。
public class ASPNETWebTestPlugin : WebTestPlugin
{
#region Fields
public const string STEP_NO_KEY = "$WEBTEST.StepNo";
private const string TEMPORARY_STORE_EXTRACTION_RULE_KEY = "$TEMP.ExtractionRules";
#endregion Fields
#region Methods
public override void PostRequest(object sender, PostRequestEventArgs e)
{
base.PostRequest(sender, e);
int stepNo = (int)e.WebTest.Context[STEP_NO_KEY];
e.WebTest.Context[STEP_NO_KEY] = stepNo + 1;
// When cookies are issues domain wide like .static.dropthings.com,
// it does not get added to the Cookie container properly so that the
// cookie is sent out on static.dropthings.com visit
foreach (Cookie cookie in e.Response.Cookies)
{
if (cookie.Domain.StartsWith("."))
{
CookieContainer container = e.WebTest.Context.CookieContainer;
cookie.Domain = cookie.Domain.TrimStart('.');
container.Add(cookie);
}
}
}
public override void PreRequest(object sender, PreRequestEventArgs e)
{
base.PreRequest(sender, e);
e.Request.ExtractValues += new EventHandler<ExtractionEventArgs>(
Request_ExtractValues);
if (!e.WebTest.Context.ContainsKey(STEP_NO_KEY))
e.WebTest.Context[STEP_NO_KEY] = 1;
}
void Request_ExtractValues(object sender, ExtractionEventArgs e)
{
RuleHelper.WhenAspNetResponse(e.Response, () =>
{
RuleHelper.NotAlreadyExtracted<ExtractHiddenFields>(
e.Request.ExtractionRuleReferences,
() =>
{
// Extract all hidden fields so that they can be used in next
// async/sync postback Hidden fields like __EVENTVALIDATION,
// __VIEWSTATE changes after every async/sync postback. So,
// these fields need to be kept up-to-date to make subsequent
// async/sync postback
var extractionRule = new ExtractHiddenFields();
extractionRule.Required = true;
extractionRule.HtmlDecode = true;
extractionRule.ContextParameterName = "1";
extractionRule.Extract(sender, e);
});
// Extract all INPUT/SELECT elements so that they can be posted in
// (async)postback
RuleHelper.NotAlreadyExtracted<ExtractFormElements>(
e.Request.ExtractionRuleReferences,
() => new ExtractFormElements().Extract(sender, e));
// Extract all __doPostBack(...) so that the ID of the control can be
// used to make async postbacks
RuleHelper.NotAlreadyExtracted<ExtractPostbackNames>(
e.Request.ExtractionRuleReferences,
() => new ExtractPostbackNames().Extract(sender, e));
// Extract all updatepanels so that during async postback, the
// updatepanel name can be derived
RuleHelper.NotAlreadyExtracted<ExtractUpdatePanels>(
e.Request.ExtractionRuleReferences,
() => new ExtractUpdatePanels().Extract(sender, e));
});
}
PreRequest 方法在 `Context` 中引入了一个方便的 `StepNo`,使得识别特定请求在哪一步失败变得容易。然后您可以将其与 Web Test 请求进行映射,确切地看到哪一步失败了。当您有多个相同类型的请求但又无法轻松确定哪个失败了时,这非常方便。例如,您在多个地方命中 _default.aspx_,其中一个失败了。从步骤计数中,您可以看到是哪个失败了。

然后在 `Request_ExtractValues` 上,它提取以下项
- 提取所有隐藏字段,例如 `__VIEWSTATE`。
- 提取所有表单元素,例如 `INPUT` 和 `SELECT` 标签及其选定值。
- 查找按钮、链接中所有可能导致回发或异步回发的 `__doPostBack` 调用。
- 查找所有 `UpdatePanel` 名称。
第一步是提取隐藏字段。它使用 `Microsoft.VisualStudio.TestTools.WebTesting.Rules.ExtractHiddenFields` 来提取隐藏字段。这里没什么特别的。这个类创建条目,例如

下一步是使用我自己的类 `ExtractFormElements` 来提取所有 input、select 标签并查找它们的值。
[DisplayName("Extract Form Elements")]
public class ExtractFormElements : ExtractionRule
{
#region Fields
public const string FORM_ELEMENT_KEYS = "$FORM_ELEMENTS";
public const string INPUT_PREFIX = "$INPUT.";
public const string SELECT_PREFIX = "$SELECT.";
public const string VALUE_SUFFIX = ".VALUE";
private static Regex _FindInputTags = new Regex(
@"<(input)\s*[^>]*(name)=""(?<name>([^""]*))""\s*[^>]*(value)="""
+ @"(?<value>([^""]*))""",
RegexOptions.IgnoreCase
| RegexOptions.Multiline
| RegexOptions.IgnorePatternWhitespace
| RegexOptions.Compiled
);
private static Regex _FindSelectTags = new Regex(
@"<select\s*[^>]*name=""(?<name>([^""]*))""\s*[^>]*.*<option\s"
+ @"*[^>]*selected=""[^""]*""\s*[^>]*value=""(?<value>([^""]*))""",
RegexOptions.IgnoreCase
| RegexOptions.Singleline
| RegexOptions.IgnorePatternWhitespace
| RegexOptions.Compiled
);
#endregion Fields
#region Methods
public override void Extract(object sender, ExtractionEventArgs e)
{
string body = e.Response.BodyString;
List<string> formElements = new List<string>();
var processMatches = new Action<MatchCollection, string>((matches, prefix) =>
{
foreach (Match match in matches)
{
string name = match.Groups["name"].Value;
string value = match.Groups["value"].Value;
string lastPartOfName = name.Substring(name.LastIndexOf('$') + 1);
string keyName = RuleHelper.PlaceUniqueItem(e.WebTest.Context,
prefix + lastPartOfName, name);
e.WebTest.Context[keyName + VALUE_SUFFIX] = value;
// Create a name value pair in context as it is using
// the form element's name
e.WebTest.Context[name] = value;
formElements.Add(name);
}
});
processMatches(_FindInputTags.Matches(body), INPUT_PREFIX);
processMatches(_FindSelectTags.Matches(body), SELECT_PREFIX);
e.WebTest.Context[FORM_ELEMENT_KEYS] = formElements.ToArray();
}
#endregion Methods
}
这将导致以下 Context 条目,您可以在 web test 中使用它们

这些都是 INPUT 按钮,它们具有相同的服务器端名称,但 ASP.NET 为它们生成了唯一的 ID。因此,您可以使用您在代码中使用的服务器端名称来获取唯一 ID 和值。
下一步是提取所有回发链接、按钮、下拉列表更改等。任何调用 ASP.NET `__doPostback` 函数以回发到服务器的内容。我们需要知道这一点,以便获取需要回发的控件的名称。这有助于我们模拟对链接、按钮的点击。
[DisplayName("Extract PostBack Names")]
public class ExtractPostbackNames : ExtractionRule
{
#region Fields
private static Regex _FindPostbackNames = new Regex(@"__doPostBack\('(.*?)'",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase
| RegexOptions.CultureInvariant);
#endregion Fields
#region Methods
/// <summary>
/// Find all javascript:__doPostback(...) type declarations which indicates
/// all controls that support postback. It finds all such controls that support
/// postback and then stores the full client ID of the control in Context
/// using the last part of the ID as key in Context. For example:
/// $POSTBACK.1.AddNewWidget = WidgetUpdatePanel001$ctl_002$AddNewWidget
/// This way you can find a particular controls full client ID when you know only the
/// server ID of the control.
/// </summary>
/// <param name="bodyHtml">Body HTML</param>
/// <param name="context">WebTest Context</param>
public static void ExtractPostBackNames(string bodyHtml, WebTestContext context)
{
RuleHelper.NotAlreadyDone(context, "$POSTBACK.EXTRACTED", () =>
{
var matches = _FindPostbackNames.Matches(bodyHtml);
foreach (Match match in matches)
{
string fullID = match.Groups[1].Value;
string lastPartOfID = fullID.Substring(fullID.LastIndexOf('$') + 1);
string contextKeyName = "$POSTBACK." + lastPartOfID;
RuleHelper.PlaceUniqueItem(context, contextKeyName, fullID);
}
});
}
public override void Extract(object sender, ExtractionEventArgs e)
{
string bodyHtml = e.Response.BodyString;
ExtractPostBackNames(bodyHtml, e.WebTest.Context);
}
#endregion Methods
}
最后,最复杂的部分是查找页面中的所有 `UpdatePanel` 以及属于 `UpdatePanel` 的控件。在模拟点击任何控件之前,我们需要知道 `UpdatePanel`,因为我们需要将 `UpdatePanel` 名称发送到服务器,以便它知道哪个 `updatepanel` 被回发并且需要刷新。
public class ExtractUpdatePanels : ExtractionRule
{
#region Fields
public const string UPDATEPANEL_EXTRACTED_KEY = "$UPDATEPANEL.EXTRACTED";
public const string UPDATE_PANEL_COUNT_KEY = UPDATE_PANEL_PREFIX + ".COUNT";
public const string UPDATE_PANEL_DECLARATION =
"Sys.WebForms.PageRequestManager.getInstance()._updateControls([";
public const string UPDATE_PANEL_KEY = "$UPDATEPANEL";
public const string UPDATE_PANEL_POS_KEY = ".$POS";
public const string UPDATE_PANEL_PREFIX = UPDATE_PANEL_KEY + ".";
private static Regex _FindUpdatePanelRegex = new Regex(
@"\|updatePanel\|(?<name>(.*?))\|",
RegexOptions.IgnoreCase
| RegexOptions.Multiline
| RegexOptions.IgnorePatternWhitespace
| RegexOptions.Compiled
);
#endregion Fields
#region Methods
public static void ExtractUpdatePanelNamesFromHtml(string body, WebTestContext context)
{
RuleHelper.NotAlreadyDone(context, UPDATEPANEL_EXTRACTED_KEY, () =>
{
// Do not extract update panel names twice
int pos = body.IndexOf(UPDATE_PANEL_DECLARATION);
if (pos > 0)
{
// found declaration of all update panels on the page
pos += UPDATE_PANEL_DECLARATION.Length;
int endPos = body.IndexOf(']', pos);
string updatePanelNamesDelimited = body.Substring(pos, endPos - pos);
string[] updatePanelNames = updatePanelNamesDelimited.Split(',');
int updatePanelCounter = 1;
foreach (string updatePanelName in updatePanelNames)
{
// Create a unique key in the context using the UpdatePanel's Last
// part of the ID which is usually the ID specified in aspx page
string updatePanelFullId =
updatePanelName.TrimStart('\'').TrimEnd('\'').TrimStart('t');
string updatePanelIdLastPart =
updatePanelFullId.Substring(updatePanelFullId.LastIndexOf('$') + 1);
string contextKeyName = UPDATE_PANEL_PREFIX + updatePanelIdLastPart;
string keyName =
RuleHelper.PlaceUniqueItem(context, contextKeyName, updatePanelFullId);
// Store all update panels as $UPDATEPANEL.1, $UPDATEPANEL.2, ...
context[UPDATE_PANEL_PREFIX + updatePanelCounter] = updatePanelFullId;
// Find the position of the UpdatePanel
string updatePanelDivId = updatePanelFullId.Replace('$', '_');
// Look for a div with id having the updatepanel ID,
// e.g. <div id="UserTabPage_TabUpdatePanel">
string lookingFor = "<div id=\"" + updatePanelDivId + "\"";
int updatePanelDivIdPos = body.IndexOf(lookingFor);
context[UPDATE_PANEL_PREFIX + updatePanelCounter +
UPDATE_PANEL_POS_KEY] = updatePanelDivIdPos;
context[keyName + UPDATE_PANEL_POS_KEY] = updatePanelDivIdPos;
updatePanelCounter++;
}
context[UPDATE_PANEL_COUNT_KEY] = updatePanelCounter;
}
});
}
public override void Extract(object sender, ExtractionEventArgs e)
{
string body = e.Response.BodyString;
if (e.Response.ContentType.Contains("text/html"))
ExtractUpdatePanelNamesFromHtml(body, e.WebTest.Context);
else // if (e.Response.ContentType.Contains("text/plain"))
ExtractUpdatePanelNamesFromAsyncPostback(body, e.WebTest.Context);
}
private void ExtractUpdatePanelNamesFromAsyncPostback
(string body, WebTestContext context)
{
RuleHelper.NotAlreadyDone(context, "$UPDATEPANEL.EXTRACTED", () =>
{
int newUpdatePanelsAdded = 0;
foreach (Match match in _FindUpdatePanelRegex.Matches(body))
{
string updatePanelDivID = match.Groups["name"].Value;
string updatePanelFullId = updatePanelDivID.Replace('_', '$');
string updatePanelIdLastPart =
updatePanelFullId.Substring(updatePanelFullId.LastIndexOf('$') + 1);
string contextKeyName = UPDATE_PANEL_PREFIX + updatePanelIdLastPart;
string keyName = RuleHelper.PlaceUniqueItem
(context, contextKeyName, updatePanelFullId);
int countOfKeys = context.Count;
RuleHelper.PlaceUniqueItem(context, UPDATE_PANEL_KEY, updatePanelFullId);
if (context.Count > countOfKeys)
newUpdatePanelsAdded++;
}
context[UPDATE_PANEL_COUNT_KEY] =
((int)context[UPDATE_PANEL_COUNT_KEY]) + newUpdatePanelsAdded;
});
}
#endregion Methods
}
这是一个复杂的类。它从 HTML 输出中查找每个 `UpdatePanel div` 的位置,以及来自异步回发的文本输出。常规回发输出是 HTML 并且易于解析。但是来自异步回发的输出是文本格式,并且是需要不同解析逻辑的特殊格式。
这将导致 Context 中的以下条目

这有助于您从服务器端名称识别 `UpdatePanel`。例如,您可以使用 `$UPDATEPANEL.WidgetBodyUpdatePanel.1` 找到第一个 `WidgetBodyUpdatePanel`。
关于 `ASPNETWebTestPlugin` 就介绍到这里。这个插件会拦截每个请求和响应,并用有用的条目准备 Context,您可以使用这些条目来准备您的测试步骤。
下一个重要的插件是特定于请求的插件——`AsyncPostbackRequestPlugin`。您已经知道如何使用它,现在看看它是如何工作的。
public class AsyncPostbackRequestPlugin : WebTestRequestPlugin
{
#region Properties
public string ControlName
{
get; set;
}
public string UpdatePanelName
{
get; set;
}
#endregion Properties
#region Methods
public override void PostRequest(object sender, PostRequestEventArgs e)
{
base.PostRequest(sender, e);
}
public override void PreRequest(object sender, PreRequestEventArgs e)
{
base.PreRequest(sender, e);
e.Request.Headers.Add("x-microsoftajax", "Delta=true");
FormPostHttpBody formBody = e.Request.Body as FormPostHttpBody;
if (null == formBody)
{
formBody = (e.Request.Body = new FormPostHttpBody()) as FormPostHttpBody;
e.Request.Method = "POST";
}
string controlName = RuleHelper.ResolveContextValue(e.WebTest.Context,
this.ControlName);
string updatePanelName = RuleHelper.ResolveContextValue(e.WebTest.Context,
this.UpdatePanelName);
// Post all input hidden fields
string[] hiddenFieldKeyNames = e.WebTest.Context["$HIDDEN1"] as string[];
if (null != hiddenFieldKeyNames)
{
foreach (string hiddenFieldKeyName in hiddenFieldKeyNames)
formBody.FormPostParameters.Add(hiddenFieldKeyName,
e.WebTest.Context["$HIDDEN1." + hiddenFieldKeyName] as string);
RuleHelper.SetParameter(formBody.FormPostParameters, "ScriptManager1",
updatePanelName + "|" + controlName, true);
RuleHelper.SetParameter(formBody.FormPostParameters, "__EVENTTARGET",
controlName, true);
RuleHelper.SetParameter(formBody.FormPostParameters, "__ASYNCPOST",
"true", true);
}
}
#endregion Methods
}
首先,它为异步回发设置了正确的请求头。然后它收集所有隐藏字段并将其存储在 `FormPostParameters` 集合中。然后它添加三个关键条目,用于标识要使用的 `UpdatePanel`,正在回发的控件,以及将回发标识为异步回发。
就是这样,一种非常方便的测试 AJAX 网站的方法,它是可重用的,即使控件 ID 是自动生成的或移到别处也有效,可以在异步回发上工作,允许您编写非常少样板代码的测试。
还有其他一些方便的提取规则和请求插件可供使用。请查看代码文档以了解它们的工作原理。

源代码
该项目的源代码可在 Dropthings 代码库 中找到。查看 `Dropthings.Test` 项目。
结论
这个 Web Test 库将大大简化您的自动化 Web Test。使用自动化 Web Test 的好处是,您无需使用某些只能通过浏览器回放的基于浏览器的录制工具。Web Tests 可以完全在没有浏览器的情况下执行,并且您可以将 Web Tests 作为负载测试的一部分。因此,您只需编写一次 Web Test,然后就可以使用相同的 Web Tests 执行负载测试。这是我使用 Web Tests 而不是 Selenium 等基于浏览器的工具的最重要的原因,因为我可以使用相同的 Web Tests 进行负载测试。但是没有这个库,编写有用的 Web Tests 会非常痛苦。因此,希望这个库能帮助您编写出色的 Web Tests。