如果要关联百度首页输入自动提示的JSON内容,那么正则表达式要使用:
web_reg_save_param_regexp(
"ParamName=aaa",
"RegExp=[,\[]\"(.*?)\"",
"Ordinal=ALL",
SEARCH_FILTERS,
LAST);
web_url("test","URL=http://suggestion.baidu.com/su?wd=loadrunner",LAST);
如果想要获得一个没有回复帖子的帖子编号,正则表达式为:
web_reg_save_param_regexp(
"ParamName=topicid",
"RegExp=ajax_(.*)\" class.*\r\n.*\r\n.*\r\n.*<em>0</em>",
"Ordinal=1",
SEARCH_FILTERS,
LAST);
换成strtok的写法,内容如下:
char tokstr[2000]; extern char * strtok(char * string, const char * delimiters ); char * token; web_reg_save_param("string", "LB=a_ajax_", "RB=<em>0</em>", "Ord=1", "Search=NoResource", LAST); //请求部分略 strcpy(tokstr,lr_eval_string("{string}")); token = (char *)strtok(tokstr,"\""); lr_output_message(token);//输出处理后得到的帖子编号 |
A.7.7 关联函数web_reg_save_param_xpath详解
如果大家用过一些自动化工具可能会对Xpath比较熟悉。Xpath可以通过路径的方式访问到XML、HTML的任意节点位置,在关联里也可以使用这个技术来帮我们查找需要的元素。
打开Add Step添加步骤,选择web_reg_save_param_xpath函数,打开设置窗口,如图A.128所示。
图A.128 添加web_reg_save_param_xpath函数
在这里需要为Query String编写对应的Xpath查询语法,这里填写的/t/book/title是指一个XML格式中的结构。通过这个关联我们可以从:
<?xml version="1.0"?>
<t><book><auther>cloud</auther></book><book><auther>cloudB</auther></book></t>
这样的服务器返回中得到以下结果:
Action.c(14): Notify: Saving Parameter "temp_1 = A".
Action.c(14): Notify: Saving Parameter "temp_2 = B".
Action.c(14): Notify: Saving Parameter "temp_count = 2".
对于一些比较复杂的数据格式,那么怎么编写Xpath呢?这里使用FireBug来帮助我们,首先安装Firefox浏览器并且安装FireBug插件,接着在打开的页面中单击右下角的FireBug图标,切出该插件,如图A.129所示。
图A.129 在Firefox中启动FireBug
接着在浏览页面中找到自己想要的内容,通过右键菜单中的Inspect Element将这个元素定位,如图A.130所示。
图A.130 在Firebug中定位元素
接着将鼠标放到上面的工具条中,会看到对应的Xpath层次已经显示出来了,如图A.131所示。
图A.131 在Firebug中观察Xpath
这里可以通过右键菜单复制当前的Xpath字符串,也可以在下面更加准确选择,如图A.132所示。
图A.132 在Firebug中复制XPath
通过这种方式不但可以得到XML的任意位置Xpath写法,还能获得HTML的任意位置Xpath。在得到Xpath后就可以直接复制到关联函数web_reg_save_param_xpath中了。但是在LR11中该关联函数只对XML数据格式有用,对于HTML格式无法使用Xpath进行定位关联,所以,在处理HTML内容时还是推荐使用前面的两个关联函数来处理。
关于Xpath的更多写法参考官方文档http://www.w3.org/TR/xpath/。
A.7.8 关联函数的高级使用
上面详细介绍了关联的作用和关联函数的详细选项,那么在工作中除了要使用关联函数获得服务器返回以外,还能做什么呢?
例如,论坛一个版面中有20个帖子,如何实现随机单击其中某一个帖子的操作呢?回想一下关联选项Ord=All的时候关联出来的结果是不是一个参数数组?既然是参数数组,怎么从参数数组中取出一个随机的值呢?
在不同的LoadRunner版本中处理这个问题使用不同的解决方法,现在来分别了解一下具体的方案(这里关联后的参数名为link,设置Ord为All)。
1.LoadRunner 9以后系列
在LoadRunner 9以后做这个操作非常简洁,因为有了参数数组函数,所以只需要这样写就可以了:
char * siteval;
siteval =lr_paramarr_random(link)
即直接从参数数组link中取一个随机的值。
问题:如果需要随机选择前10个帖子怎么办?
Lr_paramarr_random()函数的随机范围其实是根据lr_paramarr_len()决定的。比如数组长度是20,随机值介于1~20之间,现在手动将这个参数数组设置得小一些,问题就解决了:
char * siteval;
lr_save_string("10","link_count");
siteval =lr_paramarr_random(link);
思考:如果需要随机获取关联结果中的第5至第10个对象,该如何处理呢?
在这种情况下,需要引入随机数,生成随机值为5~10的正整数,再调用lr_paramarr_idx()函数进行处理即可。
如果使用的是LoadRunner9以前的版本,没有这个参数数组函数怎么办?
2.LoadRunner 8以前的版本
假设存在关联后的参数数组为{link},数组记录总个数为20,需要取得其中的一个随机关联值可以这样写:
char linkname[100],num[100];
int randnum;
//关联和请求操作省略
//获得关联参数数目内的随机数字
randnum=rand()%atoi(lr_eval_string("{link_count}"))+1;
strcpy(linkname,"{link_");
//lr_error_message("%s",linkname);
itoa(randnum,num,10);
strcat(linkname,num);
strcat(linkname,"}");
//lr_error_message"%s",linkname);
lr_save_string(lr_eval_string(linkname),"temp");
这段代码看起来是比较头疼的,因为涉及了很多新的函数。
atoi()类型强制转换函数的作用是将字符串型的内容转化为整数型。
atoi(字符串);
由于使用求余操作是对一个数字进行操作,所以需要使用atoi将参数转化为正整数。
使用Rand()%atoi(lr_eval_string("{link_count}")可以得到0~19的随机正整数。
strcpy()字符复制函数就是将一个字符串复制到一个变量中去。
strcpy(变量名,需要复制的字符串内容);
所以,strcpy(linkname,"{link_"}的作用是将"{link_"这个内容保存到变量linkname中。
itoa()也是一个强制类型转换函数,与atoi()相反,它是将整数型内容转化为字符串型。
itoa(数字型,字符串,转化格式);
这里转化格式使用的是十进制。为了拼接参数数组,需要生成"{link_2}"这样的参数,通过前面的随机函数已经生成1~20的随机正整数,现在需要把这个数字拼接上去,这里使用strcat()来实现,由于strcat()必须使用字符串,所以需要将随机整数randnum转化成字符串型的num。
strcat()是一个字符添加函数,它将一个字符串附加在一个变量后。
strcat(变量名,字符串);
接着将上面生成的字符串继续拼接到linkname变量中。
这样就拼接出了linkname="{link_2}",在讲述参数化的时候提到过参数和变量的调用,当变量这样写的时候就可以直接读取到参数名的值。所以,使用lr_eval_string()可以将这个变量对应的参数值取出,再通过lr_save_string()将值存放到另一个参数temp中去,最后参数temp就存放了{link_2}参数所对应的值。
如果需要得到1~10的随机记录呢?这时只需要在随机数生成时"做点手脚"就行了。
//获得关联参数数目内的随机数字
rndnum=rand()%atoi(lr_eval_string("{link_count}"))+1;
把这段代码修改为:
rndnum=rand()%10+1;//得到1~10的随机数字
在这里最好先做一个判断,避免出现帖子少于10个的问题。
如果需要得到5~10的随机记录呢?这时还是随机数生成的操作,如何生成一个范围内的随机数呢?通过公式rand()%(max-min+1)+min能生成从最小值到最大值之间的随机数,所以只需要写为以下形式即可:
Rndnum=rand()%(10-5+1)+5;
3.LoadRunner 8系列
到了LoadRunner 8.x系列,VuGen提供了一个新的函数来帮助我们快捷地处理类似的类型转化操作。这个函数就是sprintf(),sprintf()和C语言中的printf()函数十分相似,使用它可以生成带格式的字符串,从而帮助我们快捷地完成一个特殊格式拼接过程。
sprintf(变量名,格式,值)
例如,可以这样写:
char temp[100];
sprintf(temp,"welcome Loadrunner%d ",11);
lr_error_message(temp);
可以看到结果是"welcome loadrunner11",通过这个函数将数字11拼接到了这个字符串末尾。
还是前面的脚本,看看在LoadRunner 8.x中怎么写,现在可以将脚本改为以下形式:
char linkname[100];
int randnum;
//获得关联参数数目内的随机数字
randnum=rand()%atoi(lr_eval_string("{link_count}"))+1;
sprintf(linkname,"{link_%d}",rndnum);
lr_save_string(lr_eval_string(linkname),"temp");
这样直接就把随机的rndnum变量放在了linkname变量中,并且生成linkname= "{link_2}"这样的变量。后面的按照LoadRunner 7系列的做法就行了,是不是方便了 很多呢?
有些时候我们需要对一个字符串做类似于关联的做法,那么可以使用这样一个函数来帮助大家解决对字符串做左右边界关联的问题:
void fdk_save_search_LRBound_string(char *buf, char *leftBound, char *rightBound, char *PreFix) { /************************************ 说明:搜索字符串,通过前后匹配的方式 参数: *buf = 原字符串 *leftBound = 左边界 *rightBound = 右边界 *PreFix = 输出参数集 copyright 2009/07/07 by Fin. *************************************/ int i,j,k,intTmp, offsetA, offsetB; char *strTmpA; char *strTmpB; char *strTmpC; char *strTmpD; char *strPos; intTmp = strlen(buf) + 1; if ((strTmpA = (char *)malloc(intTmp * sizeof(char))) == NULL) { lr_output_message("Insufficient memory available"); return; } if ((strTmpB = (char *)malloc(intTmp * sizeof(char))) == NULL) { lr_output_message("Insufficient memory available"); return; } if ((strTmpC = (char *)malloc(intTmp * sizeof(char))) == NULL) { lr_output_message("Insufficient memory available"); return; } if ((strTmpD = (char *)malloc(intTmp * sizeof(char))) == NULL) { lr_output_message("Insufficient memory available"); return; } i = 0; strPos = (char *)strstr(buf, leftBound); if(strPos != NULL){ offsetA = (int)(strPos - buf); } else offsetA = -1; strPos = (char *)strstr(buf, rightBound); if(strPos != NULL) offsetB = (int)(strPos - buf); else offsetB = -1; if((offsetA < 0)||(offsetB < 0)){ sprintf(strTmpC, "%s_count", PreFix); lr_save_string ("0", strTmpC); free(strTmpA); free(strTmpB); free(strTmpC); free(strTmpD); return; } if(offsetA < offsetB){ memcpy(strTmpA, buf + offsetA + strlen(leftBound), offsetB - offsetA - strlen(leftBound)); sprintf(strTmpA + offsetB - offsetA - strlen(leftBound), "\x0"); i++; sprintf(strTmpC, "%s_%d", PreFix, i); lr_save_string (strTmpA, strTmpC); if(strlen(buf) > (offsetB + strlen(rightBound))){ memcpy(strTmpB, buf + offsetB + strlen(rightBound), strlen(buf) - offsetB - strlen(rightBound)); sprintf(strTmpB + strlen(buf) - offsetB - strlen(rightBound), "\x0"); } else sprintf(strTmpB, ""); } if(offsetA > offsetB){ memcpy(strTmpB, buf + offsetB + strlen(rightBound), strlen(buf) - offsetB - strlen(rightBound)); sprintf(strTmpB + strlen(buf) - offsetB - strlen(rightBound), "\x0"); } while(strcmp(strTmpB, "") != 0){ strPos = (char *)strstr(strTmpB, leftBound); if(strPos != NULL) offsetA = (int)(strPos - strTmpB); else offsetA = -1; strPos = (char *)strstr(strTmpB, rightBound); if(strPos != NULL) offsetB = (int)(strPos - strTmpB); else offsetB = -1; if((offsetA < 0)||(offsetB < 0)){ sprintf(strTmpC, "%s_count", PreFix); sprintf(strTmpD, "%d", i); lr_save_string (strTmpD, strTmpC); free(strTmpA); free(strTmpB); free(strTmpC); free(strTmpD); return; } if(offsetA < offsetB){ memcpy(strTmpA, strTmpB + offsetA + strlen(leftBound), offsetB - offsetA - strlen(leftBound)); sprintf(strTmpA + offsetB - offsetA - strlen(leftBound), "\x0"); i++; sprintf(strTmpC, "%s_%d", PreFix, i); lr_save_string (strTmpA, strTmpC); sprintf(strTmpC, "%s_count", PreFix); sprintf(strTmpD, "%d", i); lr_save_string (strTmpD, strTmpC); if(strlen(buf) > (offsetB + strlen(rightBound))){ memcpy(strTmpC, strTmpB + offsetB + strlen(rightBound), strlen(strTmpB) - offsetB - strlen(rightBound)); sprintf(strTmpC + strlen(strTmpB) - offsetB - strlen(rightBound), "\x0"); } else sprintf(strTmpC, ""); } if(offsetA > offsetB){ memcpy(strTmpC, strTmpB + offsetB + strlen(rightBound), strlen(strTmpB) - offsetB - strlen(rightBound)); sprintf(strTmpC + strlen(strTmpB) - offsetB - strlen(rightBound), "\x0"); } sprintf(strTmpB, "%s", strTmpC); } free(strTmpA); free(strTmpB); free(strTmpC); free(strTmpD); return; } |
最后我们来看一个比较复杂的脚本,录制一个用户登录在论坛上发帖,帖子中带一个图片附件的脚本。回放这个脚本是无法成功的,如何能够让这个用户行为成功模拟呢?大家可以先自己试试分析能不能做出来再看后面的分析及脚本。
首先录制脚本后,我们对整个代码进行分析会关注一下两个函数步骤:
web_submit_data("job.php_2", "Action=http://localhost:8000/phpwind85/job.php?action=mutiupload&random=47&photoid=0&aid=0&t=1317817943249", "Method=POST", "EncType=multipart/form-data", "TargetFrame=", "RecContentType=text/xml", "Referer=", "Snapshot=t27.inf", "Mode=HTML", ITEMDATA, "Name=Filename", "Value=7.gif", ENDITEM, "Name=verify", "Value=2c520aca", ENDITEM, "Name=uid", "Value=2620", ENDITEM, "Name=step", "Value=2", ENDITEM, "Name=desc", "Value=7.gif", ENDITEM, "Name=fid", "Value=5", ENDITEM, "Name=Filedata", "Value=7.gif", "File=Yes", ENDITEM, "Name=Upload", "Value=Submit Query", ENDITEM, EXTRARES, "Url=attachment/Mon_1110/5_2620_96be14032a268aa.gif", "Referer=http://localhost:8000/phpwind85/post.php?fid=5", ENDITEM, "Url=js/breeze/util/localStorage.js?v=1.0", "Referer=http://localhost:8000/phpwind85/post.php?fid=5", ENDITEM, "Url=images/loading.gif", "Referer=http://localhost:8000/phpwind85/post.php?fid=5", ENDITEM, LAST); web_submit_data("post.php_2", "Action=http://localhost:8000/phpwind85/post.php?fid=5&nowtime=1317817952576&verify=3985a50d", "Method=POST", "EncType=multipart/form-data", "TargetFrame=", "RecContentType=text/xml", "Referer=http://localhost:8000/phpwind85/post.php?fid=5", "Snapshot=t28.inf", "Mode=HTML", ITEMDATA, "Name=magicname", "Value=", ENDITEM, "Name=magicid", "Value=", ENDITEM, "Name=verify", "Value=3985a50d", ENDITEM, "Name=cyid", "Value=0", ENDITEM, "Name=ajax", "Value=1", ENDITEM, "Name=iscontinue", "Value=0", ENDITEM, "Name=atc_title", "Value=uploadimg", ENDITEM, "Name=atc_content", "Value=[attachment=12]uploadimg", ENDITEM, "Name=flashatt[12][desc]", "Value=", ENDITEM, "Name=flashatt[12][special]", "Value=无", ENDITEM, "Name=flashatt[12][ctype]", "Value=铜币", ENDITEM, "Name=flashatt[12][needrvrc]", "Value=", ENDITEM, "Name=isAttachOpen", "Value=1", ENDITEM, "Name=atc_usesign", "Value=1", ENDITEM, "Name=atc_autourl", "Value=1", ENDITEM, "Name=atc_convert", "Value=1", ENDITEM, "Name=atc_money", "Value=0", ENDITEM, "Name=atc_credittype", "Value=money", ENDITEM, "Name=atc_rvrc", "Value=0", ENDITEM, "Name=atc_tags", "Value=", ENDITEM, "Name=step", "Value=2", ENDITEM, "Name=pid", "Value=", ENDITEM, "Name=action", "Value=new", ENDITEM, "Name=fid", "Value=5", ENDITEM, "Name=tid", "Value=0", ENDITEM, "Name=article", "Value=0", ENDITEM, "Name=special", "Value=0", ENDITEM, "Name=_hexie", "Value=96ef2436", ENDITEM, LAST); |
这两个函数应该分别对应了文件上传请求和发帖请求两个步骤,里面存在了我们前面说到必须要关联的两个关键字_hexie和verify,所以首先对这两个属性补充关联。回放脚本,出现错误,连运行自动关联的机会都没有,要不改用手工关联解决?不过这个错误和关联没关系啊。仔细检查错误会发现错误的原因是需要上传的7.gif文件找不到导致的。
我们录制的时候图片文件是在硬盘的某一个位置的,但是在上传的时候请求中并没有包含路径(这是一种保护客户隐私的好做法)。当脚本回放的时候,LR会检查当前脚本目录,由于该文件不存在导致了回放失败。解决该问题有两种方式:第一种是将该文件放在脚本目录下,第二种是修改代码强行指定绝对路径。这里我们将需要添加的7.gif存放到C盘根目录,并为代码添加绝对路径,代码修改为(这里为了演示写法选择了最复杂的做法,推荐大家直接把文件放在脚本目录下):
Web_submit_data("job.php_2", "Action=http://localhost:8000/phpwind85/job.php?action=mutiupload&random=47&photoid=0&aid=0&t=1317817943249", "Method=POST", "EncType=multipart/form-data", "TargetFrame=", "RecContentType=text/xml", "Referer=", "Snapshot=t27.inf", "Mode=HTML", ITEMDATA, "Name=Filename", "Value=7.gif", ENDITEM, "Name=verify", "Value=2c520aca", ENDITEM, "Name=uid", "Value=2620", ENDITEM, "Name=step", "Value=2", ENDITEM, "Name=desc", "Value=7.gif", ENDITEM, "Name=fid", "Value=5", ENDITEM, "Name=Filedata", "Value=c:\\7.gif", "File=Yes", ENDITEM, "Name=Upload", "Value=Submit Query", ENDITEM, EXTRARES, "Url=attachment/Mon_1110/5_2620_96be14032a268aa.gif", "Referer=http://localhost:8000/phpwind85/post.php?fid=5", ENDITEM, "Url=js/breeze/util/localStorage.js?v=1.0", "Referer=http://localhost:8000/phpwind85/post.php?fid=5", ENDITEM, "Url=images/loading.gif", "Referer=http://localhost:8000/phpwind85/post.php?fid=5", ENDITEM, LAST); |
接着我们重新运行代码,文件无法找到的错误被解决,然后我们尝试使用自动关联。修改一下登录的用户名,这样让回放的_kexie和verify属性发生变化便于自动关联。这个时候会发现需要关联的内容有5项之多,除了前面要的这两个还有一些动态属性并不太清楚含义,所以自动关联一方面给了我们很多便利,另外一方面也让我们失去了更仔细了解业务的机会。根据前面的经验,我们先把非常确定的_hexie和verify关联上。接着我们再回放一次脚本,这个时候会发现帖子发出来了,但是并没有带图片,那么还缺什么呢?接着要分析一下是不是上传图片的时候也有动态数据。
通过检查Test Result,我们会发现在上传附件这个请求的返回中,存在这样的XML数据包:
<?xml version="1.0" encoding="utf-8"?>
<ajax>
<![CDATA[{"aid":"16","path":"attachment/Mon_1110/5_2620_a9b2d4f8c2bf150.gif"}]]>
</ajax>
通过分析我们可以得知aid是生成附件的唯一ID,而后面的串是说明存放在服务上的相对路径。接着我们在浏览器中访问Phpwind站点下的相对路径或者到xampp下的phpwind项目目录找找看有没有这个文件。这里会发现这个文件并没有被正确地上传到服务器上。虽然返回了aid和path,但是服务器上并没有这个文件。所以在这里要完成一个上传到脚本遇到了一个很大的困难,在提交数据的时候出了什么问题?为什么服务器已经返回了aid和物理地址,但是服务器上却没有这个文件呢?
在性能测试脚本开发中会经常遇到类似的问题,当遇到这种问题的时候,认真分析一下往往会发现这些问题都是因为功能测试不完善导致的。当我们抛一个不太规范的数据包给服务器的时候,被测服务能否正确地校验提交数据的合法性。不合法的数据应该不能简单地让系统自动出现500错误,而是应该返回对应的错误状态码。当提交数据合法,并且业务处理完成后返回需要的信息,操作失败时需要返回操作失败的信息,便于客户端状态控制。
那么现在我们怎么解决附件上传失败的问题呢?首先需要进一步分析提交文件上传,使用HttpWatch录制一个添加附件上传的过程,你会惊讶地发现,在所有的HTTP请求中并没有出现意料中的上传POST请求,也就说在这里上传附件并不是走的HTTP协议,而LoadRunner能够录制到上传的web_submit_data函数是"神来之笔"。既然不是HTTP协议,那么我们使用HTTP的协议来开发这个脚本就是牛头不对马嘴了,这也是为什么文件上传失败的根本原因。
Phpwind85到底是如何完成上传的呢?注意上传的对话框是一个Flash的窗体,并不是传统基于HTML的表单提交型文件上传,所以文件的上传是由Flash客户端负责完成的。当点击文件上传,会产生一个由Flash客户端生成的文件上传连接,而文件上传成功后,服务器会返回对应文件生成的服务器路径及附件的附件编号。那么在知道文件上传的方式后脚本文件上传失败的原因我们就找到了,使用HTTP协议是无法解决这个问题的,如果在没有开发协助的情况下想要解决这个问题,就需要另辟蹊径了。
进入Phpwind85后台设置,检查一下附件上传的设定。使用管理员登录后单击"系统设置"链接,进入后台管理页面,再次登录进入后台,在全局的附件设置项中提供了对于整个论坛的附件策略设置。这里会发现除了标准的上传策略以外,Phpwind85还提供了基于FTP方式上传的功能,而LoadRunner是支持FTP协议的,那么我们可以这样来解决上传的问题:
对于页面操作,我们还是使用HTTP协议来实现用户行为模拟。
对于文件上传,我们使用FTP协议来上传文件。
这样我们就可以完成整个用户登录发帖,发帖中带有上传图片的操作,并且上传图片会在帖子中显示的用户行为模拟。由于这里使用了多协议混合的情况,具体的解决方式及相关请看8.9节使用HTTP+FTP协议解决附件上传。
补充:关联中常用的转义内容(见表A.15)
表A.15 关联中常用的转义内容
关联的使用要点:
1)什么内容需要关联
当脚本中的数据每次回放都发生变化时,并且这个动态数据在后面的请求中需要发送给服务器,那么这个内容就需要通过关联来询问服务器,获得该数据的变化结果。当确认关联的内容后,要确认所需要的数据是在哪个请求中返回的,并且定位该数据的位置和特征,以及左右边界。
2)关联的边界如何设置
左右边界是定位动态数据的关键,但是想要精确定位数据的左右边界并不是一件容易的事,可以通过先松后紧的方式来达到这个目标。首先在做关联的时候设置一个比较明确固定的值,确保能够将所需要的内容整体保存为参数,其次再根据返回的内容进一步细化边界值,一步步达到准确关联结果的目的。使用正则表达式关联可以让这个问题化繁为简。
版权声明:51Testing软件测试网获作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责任。
相关文章: