Rational Robot可被用来对包含数据关联的复杂Web应用进行性能测试。这里所谓数据关联,是指Web页面之间存在的数据相关性,例如一个动态的页面URL或者个别输入参数需要从前一个页面中抽取出来,有时候还需要在抽取得到的结果的基础上做进一步处理。这就使得测试开发员通常必须对Robot自动生成的VU脚本进行修改从而保证其能正确运行。简单情形下,VU语言库提供的一些库函数可以支持常见的抽取需求。但在很多更复杂的情形中,往往需要通过更多的编程来处理页面之间的数据关联,包括进行模式匹配、模拟Java');" target="_self">Java Script或者Java Applet的行为等。本文将介绍处理最常见的几种数据关联的方法,并提供了一系列很有用的功能函数,帮助测试开发员编写更具灵活性的VU脚本。
简介
随着越来越多的企业应用被移植到Web上,Web应用正变得日益复杂。它们被用来实现复杂的业务流程例如交易甚至工作流。一个业务流程通常包含若干步骤。这些步骤间自然地需要共享某些数据以完成一次连续的“计算”。例如,某一个步骤的输出可能是下一个步骤所需的输入。在一个典型的Web应用实现中,业务流程的每个步骤对应为一个HTML页面,因而最终用户将与一系列连续的页面依次交互以完成一个完整的业务流程。由于Web的无状态特性,这些页面中通常需要存储一些信息来实现它们之间所需的数据共享,例如下一个页面的URL以及其他可能的输入参数等。这些信息常常是由服务器动态生成,因此每次的值都可能不同。但是,当Robot录制一个HTTP会话时,它只能记录这些数据在这个会话中的一个快照。尽管Robot采用了一种称为“动态数据关联”(Dynamic data correlation)的技术使得它能够关联部分动态的值,但还是无法找出所有需要关联的值并据此产生具备完善功能和足够灵活性的VU脚本。即使Robot可以简单地认为所有的数据都是动态的,如何在可用的HTML页面中抽取甚至构造这些数据的值则是一个更加复杂和困难的问题,因为Robot对这些数据后隐含的逻辑一无所知。因此,在Robot不能产生令人满意的VU脚本时,就需要手工修改进行完善。
下面将首先对Web应用中的数据关联作更进一步的剖析,接着介绍如何使用Robot的“动态数据关联”技术,然后详细讨论当Robot不能产生满意的脚本时一些可能的解决方案,包括动态数据值的定制抽取和客户端数据构造的模拟等。
Web应用中数据关联的分析
在Web应用中,当一个特定的HTML页面的URL或者个别输入参数的值是动态产生因而必须从先于它返回的页面包含的数据中抽取或者构造出来时,就发生了数据关联。动态输入参数的一个很好的例子就是当前很普遍的“Session ID”,它由服务器生成并返回给用户的浏览器,在访问下一个页面时这个ID需要被发送回去以获取存储在服务器端的会话上下文。输入参数通常以四种方式提交:HTTP头参数、Cookie、URL参数和FORM参数。由于URL参数可被认为是URL的一部分,因此可以认为有四种可能发生关联的动态数据:HTTP头参数、Cookie、URL、FORM参数。在Robot的VU语言中,一个HTTP请求是通过调用库函数“http_request”发出的,列表1是给出了一个典型的用例。请注意列表1中各粗体部分,它们分别代表了四种可能发生关联的动态数据的形式中的一种。
列表1. 函数http_request的典型用例
http_request ["t3079"]
"POST /pkmslogin.form HTTP/1.1\r\n"
"Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, applicat"
"ion/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, ap"
"plication/x-shockwave-flash, */*\r\n"
"Referer: " + SgenURI_009 + "\r\n"
/* "Referer: http://gclgtod.cn.ibm.com/wps/myportal? lang=en_US" */
"Accept-Language: en-us,zh-cn;q=0.5\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)\r\n"
"Host: gclgtod.cn.ibm.com\r\n"
"Content-Length: 55\r\n"
"Connection: Keep-Alive\r\n"
"Cache-Control: no-cache\r\n"
"Cookie: w3sauid=d002000000001363710753854620000923482.0009B551AB; PBC_N LSP"
"=en_US; msp=alreadyOffered; JSESSIONID=0000fRBw1aq9nolhnP9ZMKhaw2B:- 1; "
"PD-H-SESSION-ID=4_oxjUZgfvY4ToFOhh9cFnnAg54o4sndHOA6rRkqpxbT2NAAAA\r"
"\n"
"\r\n"
"username=admin&password=admin&login-form-type="
+ http_url_encode(SgenRes_005[0]) + "";
|
正确处理数据关联的第一步是使用变量替换Robot录制的脚本中包含的动态数据的静态值,这些变量将在脚本运行时被动态地赋值。以列表1中的HTTP头参数“Referer”和FORM参数“login-form-type”为例,它们都由一个变量来赋值。但接下来的难题是:如何得到这些变量的值?一般有两种可能:一种是它们的值被直接包含在返回的HTTP响应中(包括响应的头和HTML内容)因而可以通过字符串抽取获得;另一种则需要进一步在抽取得到的若干值的基础上进行构造来获得。Robot能够自动地识别并抽取某些类型的动态值,这将在下一节中进行介绍。然而,目前它还不能发现所有这些动态值,更不用说根据一个未知的逻辑去构造一个值。因此,通常需要测试开发人员通过对VU脚本进行编程来定制变量值的抽取过程或者模拟某个数据构造过程。
这里需要澄清的是,动态数据和需要使用数据池(Datapool)的数据是不一样的。后者基本上是为了模仿最终用户的输入,它的值可以在脚本运行前确定并加载到数据池中(例如用户名和密码)。而本文中所指的动态数据大多是由服务器在运行时间生成和返回并需要在后续请求中以某种形式发送回去。不过,在某些非常特殊的情形下,例如当服务器对某个动态数据只生成一个有限集合的值(例如true和false)并且对用户会话不敏感,那么可以使用一个加载了所有这些可能值的数据池用于该动态数据的赋值,前提是能与服务器生成这些值类似的“逻辑”来从数据池中获得这些值(例如随机方式)。
使用Robot的“动态数据关联”功能
如前所述,关联的动态数据可以有四种形式提交给服务器,下面介绍Robot如何分别处理它们。
HTTP头参数
HTTP头参数产生关联的情形较其他几种形式少,但是有一个例外,即“Referer”头参数。根据HTTP协议,该参数的值应该指向上一个被访问的URL地址。由于URL的动态性,“Referer”成为一个非常普遍的需要进行关联的动态数据,因此Robot特别定义了一个名为“_reference_URI”的只读系统变量,用来存储上一个GET或者POST命令请求的完整URL地址。在其生成的脚本中,Robot会自动地用变量“_reference_URI”替换所有“Referer”头参数的值。
Cookie
Cookie被广泛用来传递关联的动态数据,例如会话ID。使用Cookie的形式提交动态数据的好处是其值往往来自Cookie自身。具体地讲,如果服务器选择使用Cookie来存储一些动态数据,它会使用“Set-Cookie”语句在HTTP响应头中指定这些数据和它们的值。在浏览器发出下一个请求时,只要简单地将这些Cookie包含在HTTP请求头中发送回去。Cookie的这种特性使得Robot能方便地处理它们携带的动态数据。
Robot能处理两种不同的Cookie:浏览器存储的Cookie和动态Cookie。对于浏览器存储的Cookie,Robot会在录制一个HTTP会话前查询浏览器存储的所有Cookie并将它们放在最后生成的脚本的COOKIE_CACHE部分中。Robot会把这些Cookie的过期日期设置为足够远的将来以保证在脚本运行的时候它们不会过期。当脚本被回放时,COOKIE_CACHE中的Cookie都会被加载到内存中使得回放过程尽量符合实际情况。整个过程都由Robot自动完成,不需测试开发人员干预。对于动态Cookie,即那些在录制脚本时由服务器返回的Cookie,Robot会将它们作为HTTP请求头的一部分保存在生成的脚本中(见列表1),但是在脚本被回放时它们的值会被替换为服务器实际返回的值——如果服务器确实有返回的话,否则就使用脚本中记录的值。
URL
从动态性的角度考虑,一个URL可以被分割为两个部分:出现在“?”前的location部分,和出现在“?”后的可选的search部分。后者用于携带若干个URL参数因此比前者更具动态性。由于URL参数和FORM参数的处理非常类似,将下一小节中一起讨论。对于相对比较静态的location部分,Robot一般只简单地保存录制到的值而不会做任何处理。但不幸的是在实际情况中URL的location部分也可能是动态地生成的,一个很好的例子就是WebSphere Portal Server生成的URL链接,在这种情况下就需要通过编程定制的字符串抽取来获得。
FORM参数
很多Web应用并不区分FORM 参数和URL参数。实际上,当一个FORM以GET方法提交的时候,它的参数就变为URL参数了。从对数据关联的意义上讲,也可以认为两者没有区别,除了在提交的时候它们位于HTTP请求中的不同位置:URL参数作为URL的一部分出现在HTTP请求头中,而FORM参数则出现在内容中。Robot采用一种名为“动态数据关联”的技术来完成部分参数的自动关联。通过以下步骤可以激活这一功能:
1. 点击菜单“Tools”->“Session Record Options”;
2. 点击“Generator per Protocol”标签,见图1;
3. 在“Correlate variables in response”设置区中,选择以下选项之一:
a. All - 关联所有可识别的变量。
b. Specific - 只关联指定的变量。通过设置区中的“Add”和“Remove”按钮来指定需要关联的参数的名称。
c. None - 不关联任何变量。
图1. 设置Robot的自动关联功能
如果选择了“All”或者“Specific”选项,那么生成的VU脚本中会包含若干对库函数“http_find_values”的调用。该库函数会找出由服务器返回并且最终用户不作修改的参数,然后抽取出它们的值并保存在一系列以“SgenRes_nnn”形式命名的变量中。举例来讲,列表2中包含了一个隐藏的FORM参数“mode”。Robot会确定该参数需要进行关联并生成相应的脚本代码(见列表3)来动态抽取它的值。
列表 2. 一个FORM样例
列表 3. Robot生成的VU脚本片断样例
…
{
string SgenRes_001[];
SgenRes_001 = http_find_values("mode", HTTP_FORM_DATA, 1);
CHECK_FIND_RESULT(SgenRes_001,"mode","simpe")
}
…
|
库函数http_find_values会在当前HTTP连接的响应中搜索所需的参数值。它的语法如下:
string[] http_find_values(name, type, tag[, name, type, tag ... ]),
|
其中name指定参数的名称,type指定参数所在的数据形式,tag指定使用符合条件的第几个参数值。type的值应为以下值之一:HTTP_FORM_DATA、HTTP_HREF_DATA或HTTP_COOKIE_DATA,分别代表FORM数据、URL数据或Cookie数据。每一个name、type和tag的组合都唯一地确定了一个单一的值,调用http_find_values时最多可以指定21个这样的组合。宏CHECK_FIND_RESULT验证它返回的值不为空,若为空则提供一个缺省值,该缺省值是在脚本录制时记录的值。
可以发现,虽然使用了动态数据关联技术,Robot还是只能从FORM数据、URL数据或者Cookie数据中抽取参数值。如果动态数据的值被包含在其他地方,例如FORM中的“action”属性中,Robot就无能为力了。
参数值的定制抽取
当URL的location部分是由服务器动态生成或者部分参数不在Robot能自动关联的范围之内时,就需要通过编程来定制参数值的抽取,简单地讲就是进行字符串匹配。VU语言的库提供了几个用于此目的的函数。除了前面已经介绍过的库函数http_find_values,库函数http_header_info可被用来从最近的HTTP响应头中抽取一个头参数。此外还有很多基本的字符串处理函数,可在它们的基础上编写更复杂的自定义函数。下面介绍几个作者编写的可用于一般目的抽取函数。
列表4中定义的“getURLByText”函数可以通过指定一个字符串获得围绕该字符串的HTML Anchor标签的HREF属性。例如,getURLByText(“
Hello world!
”, “a surprise”)将返回“hello2.jsp”。如果第二个参数变为“surprise”,则返回“hello1.jsp”,因为该函数总是返回第一个被匹配到的结果。如果没有找到任何匹配,getURLByText返回一个空字符串。
列表4. 函数getURLByText
string func getURLByText(source, text)
string source, text;
{
int startText, startA, startHref;
string remainingText, beforeText, aOpenText, hrefText, url;
string pattern;
pattern = "([ \\t\\n\\r]*\"(([^\"]*)$0)\")|";
pattern += "([ \\t\\n\\r]*\\'(([^\\']*)$0)\\')|";
pattern += "([ \\t\\n\\r]*(([^ \\t\\n\\r]*)$0)[ \\t\\n\\r>])";
remainingText = source;
while (1) {
startText = strstr(remainingText, text);
if (0 == startText) {
break;
} else {
beforeText = substr(remainingText, 1, startText - 1);
startA = 0; // Find the position of the last occurrence of "', beforeText)) {
// The anchor does enclose specified text.
if (match(
'([Hh][Rr][Ee][Ff][ \t\n\r]*=)$0',
beforeText,
&hrefText)) {
// Check the location of the found "href".
startHref = strstr(beforeText, hrefText);
if (startHref < strstr(beforeText, ">")) {
// Now try to extract the URL
if (match(
pattern,
substr(
beforeText,
startHref + strlen(hrefText),
strlen(beforeText)),
&url)) {
return url;
}
}
}
}
}
}
remainingText =
substr(
remainingText,
startText + strlen(text),
strlen(remainingText));
}
return "";
}
|
列表5中定义的函数“getURLByTextEx”提供了类似但更强大的功能。它允许使用VU语言所支持的正则表达式来指定目标Anchor标签所围绕的字符串的模式。例如,getURLByTextEx(“
Hello world!
, “[Ss]urprise”)将返回“hello1.jsp”。
列表 5. 函数getURLByTextEx
string func getURLByTextEx(source2, expression2)
string source2, expression2;
{
int startText2, startA2, startHref2;
string text2, remainingText2, beforeText2, aOpenText2, hrefText2, url2;
string newExpression2, pattern2;
pattern2 = "([ \\t\\n\\r]*\"(([^\"]*)$0)\")|";
pattern2 += "([ \\t\\n\\r]*\\'(([^\\']*)$0)\\')|"; pattern2 += "([ \\t\\n\\r]*(([^ \\t\\n\\r]*)$0)[ \\t\\n\\r>])";
newExpression2 = "(" + expression2 + ")$0";
remainingText2 = source2;
while (1) {
if (!match(newExpression2, remainingText2, &text2)) {
break;
} else {
startText2 = strstr(remainingText2, text2);
beforeText2 = substr(remainingText2, 1, startText2 - 1);
startA2 = 0; // Find the position of the last occurrence of "', beforeText2)) {
// The anchor does enclose specified text.
if (match(
'([Hh][Rr][Ee][Ff][ \t\n\r]*=)$0',
beforeText2,
&hrefText2)) {
// Check the location of the found "href".
startHref2 = strstr(beforeText2, hrefText2);
if (startHref2 < strstr(beforeText2, ">")) {
// Now try to extract the URL
if (match(
pattern2,
substr(
beforeText2,
startHref2 + strlen(hrefText2),
strlen(beforeText2)),
&url2)) {
return url2;
}
}
}
}
}
}
remainingText2 =
substr(
remainingText2,
startText2 + strlen(text2),
strlen(remainingText2));
}
return "";
}
|
列表6中定义的一系列函数可根据一个字符串的包围字符串、前缀或者后缀进行抽取。它们的名字暗示了其各自的功能。例如,假设列表2中的HTML内容被保存在系统变量“_response”中,那么通过调用getStringByBoundaries(_response, “action=\””, “\””)可以得到字符串“/search”;或者,也可以通过调用getStringByPrefixAndBoundary(_response, “/sea”, “\” name”)、getStringByBoundaryAndPostfix(_response, “action=\””, “ch”)或getStringByPrefixAndPostfix(_response, “/sea”, “ch”)来得到。这些函数也有相应的支持正则表达式的版本。
列表6. 一系列字符串抽取函数
string func getStringByBoundaries(source3, b1, b2)
string source3, b1, b2;
{
int startPos, endPos;
startPos = strstr(source3, b1);
if (0 == startPos) {
return "";
}
startPos += strlen(b1); endPos = strstr(substr(source3, startPos, strlen(source3)), b2);
if (0 == endPos) {
return "";
}
return substr(source3, startPos, endPos - 1);
}
string func getStringByPrefixAndBoundary(source4, prefix, b3)
string source4, prefix, b3;
{
int startPos2, endPos2;
startPos2 = strstr(source4, prefix);
if (0 == startPos2) {
return "";
}
endPos2 = strstr( substr(source4, startPos2 + strlen(prefix), strlen(source4)), b3);
if (0 == endPos2) {
return "";
}
return
substr(source4, startPos2, strlen(prefix) + endPos2 - 1);
}
string func getStringByBoundaryAndPostfix(source5, b4, postfix)
string source5, b4, postfix;
{
int startPos3, endPos3;
startPos3 = strstr(source5, b4);
if (0 == startPos3) {
return "";
}
startPos3 += strlen(b4);
endPos3 = strstr(
substr(source5, startPos3, strlen(source5)), postfix);
if (0 == endPos3) {
return "";
}
return
substr(source5, startPos3, strlen(postfix) + endPos3 - 1);
}
string func getStringByPrefixAndPostfix(source6, prefix2, postfix2)
string source6, prefix2, postfix2;
{
int startPos4, endPos4;
startPos4 = strstr(source6, prefix2);
if (0 == startPos4) {
return "";
}
endPos4 = strstr(
substr(source6, startPos4 + strlen(prefix2), strlen(source6)),
postfix2);
if (0 == endPos4) {
return "";
}
return substr(source6, startPos4,
strlen(prefix2) + strlen(postfix2) + endPos4 - 1);
}
|
上面介绍的所有这些函数都定义在文件“routines.s”中(见资源)。若要使用它们,请在Robot中创建一个新的空脚本然后将routines.s的内容粘贴进去。在其他脚本中,只要在文件头中添加下面这一行就可以使用上面介绍的函数了:
请将“newscript”替换为实际的文件名。
客户端数据构造的模拟
一个具备完整功能的VU脚本应该具备模仿浏览器所有相关行为的能力。举个简单的例子,仔细阅读列表1中的脚本片断会发现,把“Content-Length”这个头参数的值静态地设置为55是不恰当的,原因是实际的内容长度取决于可能出现的使用关联的动态值或者数据池的FORM参数值。因此,更好的做法是模仿浏览器在运行时计算实际的内容长度,而不是使用录制脚本时记录的静态值。列表7给出了改进后的脚本。
列表7. 改进后的脚本
{
string formData;
formData = "username=admin&password=admin&login-form-type="
+ http_url_encode(SgenRes_005[0]) + "";
}
http_request ["t3079"]
"POST /pkmslogin.form HTTP/1.1\r\n"
"Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, applicat"
"ion/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, ap"
"plication/x-shockwave-flash, */*\r\n"
"Referer: " + SgenURI_009 + "\r\n"
/* "Referer: http://gclgtod.cn.ibm.com/wps/myportal?lang=en_US" */
"Accept-Language: en-us,zh-cn;q=0.5\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)\r\n"
"Host: gclgtod.cn.ibm.com\r\n"
"Content-Length: " + itoa(strlen(formData)) + "\r\n"
"Connection: Keep-Alive\r\n"
"Cache-Control: no-cache\r\n" "Cookie: w3sauid=d002000000001363710753854620000923482.0009B551AB; PBC_N LSP"
"=en_US; msp=alreadyOffered; JSESSIONID=0000fRBw1aq9nolhnP9ZMKhaw2B:- 1; "
"PD-H-SESSION-ID= 4_oxjUZgfvY4ToFOhh9cFnnAg54o4sndHOA6rRkqpxbT2NAAAA\r"
"\n"
"\r\n"
"" + formData + "";
|
当HTML页面中包含JavaScript代码或者嵌入式组件例如Java Applet和ActiveX控件时,客户端的模拟就变得更为重要,因为它们常会被用来根据某一逻辑响应用户的操作在客户端动态地生成一些数据的值。客户端模拟的最直接有效的方法就是用VU语言实现由网页中的JavaScript或者嵌入式组件所实现的数据构造过程。但在这之前,通常需要先抽取构造所需的输入,上一节中介绍的函数会有助于此。举个例子,列表8中的HTML页面片断使用JavaScript来根据一个员工的名字动态地生成一个编码后的URL,列表9中的VU脚本片断模拟了这一URL的构造过程。
列表8. 包含JavaScript代码的HTML片断样例
列表9. 模拟JavaScript的VU脚本片断样例
…
#include "routines.s"
…
{
string employeeName;
employeeName = getStringByBoundaries(_response, "javascript:gotoPage('", "'");
employeeName += ".htm";
employeeName = http_url_encode(employeeName);
}
…
|
总结
Web应用中的连续页面存在数据关联是很普遍的现象。使用Rational Robot通过或多或少的人工干预可以正确地处理这些关联从而产生更完善的VU脚本。本文在分析了常见形式的数据关联的基础上介绍了其相应的处理方法。