Rational Robot
可被用来对包含数据关联的复杂
Web
应用进行性能测试。这里所谓数据关联,是指
Web
页面之间存在的数据相关性,例如一个动态的页面
URL
或者个别输入参数需要从前一个页面中抽取出来,有时候还需要在抽取得到的结果的基础上做进一步处理。这就使得测试开发员通常必须对
Robot
自动生成的
VU
脚本进行修改从而保证其能正确运行。简单情形下,
VU
语言库提供的一些库函数可以支持常见的抽取需求。但在很多更复杂的情形中,往往需要通过更多的编程来处理页面之间的数据关联,包括进行模式匹配、模拟
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
中各粗体部分,它们分别代表了四种可能发生关联的动态数据的形式中的一种。
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
如何分别处理它们。
HTTP
头参数
HTTP
头参数产生关联的情形较其他几种形式少,但是有一个例外,即
“Referer”
头参数。根据
HTTP
协议,该参数的值应该指向上一个被访问的
URL
地址。由于
URL
的动态性,
“Referer”
成为一个非常普遍的需要进行关联的动态数据,因此
Robot
特别定义了一个名为
“_reference_URI”
的只读系统变量,用来存储上一个
GET
或者
POST
命令请求的完整
URL
地址。在其生成的脚本中,
Robot
会自动地用变量
“_reference_URI”
替换所有
“Referer”
头参数的值。
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
可以被分割为两个部分:出现在
“?”
前的
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
-
不关联任何变量。
如果选择了
“All”
或者
“Specific”
选项,那么生成的
VU
脚本中会包含若干对库函数
“http_find_values”
的调用。该库函数会找出由服务器返回并且最终用户不作修改的参数,然后抽取出它们的值并保存在一系列以
“SgenRes_nnn”
形式命名的变量中。举例来讲,列表
2
中包含了一个隐藏的
FORM
参数
“mode”
。
Robot
会确定该参数需要进行关联并生成相应的脚本代码(见列表
3
)来动态抽取它的值。
…
<form action="/search" name="frmSearch" method="GET">
<input type="hidden" name="mode" value="simple">
<input maxLength="256" size="55" name="keyword" value=""><br>
<input type="submit" value="Search" name="btnSearch">
</form>
…
|
…
{
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(“<p>Hello world! <a href href=hello1.jsp><img src=surprise.jpg></a><a href=hello2.jsp>Click here for a surprise!</a></p>”, “a surprise”)
将返回
“hello2.jsp”
。如果第二个参数变为
“surprise”
,则返回
“hello1.jsp”
,因为该函数总是返回第一个被匹配到的结果。如果没有找到任何匹配,
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 "<a" or "<A".
while (match('(<[aA][ /t/n/r]+)$0', beforeText, &aOpenText)) {
startA = strstr(beforeText, aOpenText);
beforeText = substr(beforeText, startA + 2, strlen(beforeText));
}
if (0 < startA) {
if (!match('</[aA][ /t/n/r]*>', 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(“<p>Hello world! <a href href=hello1.jsp><img src=surprise.jpg></a><a href=hello2.jsp>Click here for a surprise!</a></p>, “[Ss]urprise”)
将返回
“hello1.jsp”
。
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 "<a" or "<A".
while (match('(<[aA][ /t/n/r]+)$0', beforeText2, &aOpenText2)) {
startA2 = strstr(beforeText2, aOpenText2);
beforeText2 = substr(beforeText2, startA2 + 2, strlen(beforeText2));
}
if (0 < startA2) {
if (!match('</[aA][ /t/n/r]*>', 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”)
来得到。这些函数也有相应的支持正则表达式的版本。
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
的内容粘贴进去。在其他脚本中,只要在文件头中添加下面这一行就可以使用上面介绍的函数了:
#include “newscript.s”
|
请将
“newscript”
替换为实际的文件名。
一个具备完整功能的
VU
脚本应该具备模仿浏览器所有相关行为的能力。举个简单的例子,仔细阅读列表
1
中的脚本片断会发现,把
“Content-Length”
这个头参数的值静态地设置为
55
是不恰当的,原因是实际的内容长度取决于可能出现的使用关联的动态值或者数据池的
FORM
参数值。因此,更好的做法是模仿浏览器在运行时计算实际的内容长度,而不是使用录制脚本时记录的静态值。列表
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
的构造过程。
…
<script language="JavaScript">
<!--
function URLEncode(aURL) {
// Encode a URL.
…
}
function gotoPage(employeeName) {
employeeName += ".htm";
employeeName = URLEncode(employeeName);
document.location.href = employeeName;
}
//-->
</script>
Jack Lee
<input type="button" onClick="javascript:gotoPage('Jack Lee')"
value="View profile" /><br>
Rose Smith
<input type="button" onClick="javascript:gotoPage('Rose Smith')"
value="View profile" /><br>
…
|
…
#include "routines.s"
…
{
string employeeName;
employeeName = getStringByBoundaries(_response, "javascript:gotoPage('", "'");
employeeName += ".htm";
employeeName = http_url_encode(employeeName);
}
…
|
Web
应用中的连续页面存在数据关联是很普遍的现象。使用
Rational Robot
通过或多或少的人工干预可以正确地处理这些关联从而产生更完善的
VU
脚本。本文在分析了常见形式的数据关联的基础上介绍了其相应的处理方法。