其实无论用那种方法,接口测试的原理是通过测试程序模拟客户端向服务器发送请求报文,服务器接收请求报文后对相应的报文做出处理然后再把应答报文发送给客户端,客户端接收应答报文这一个过程。
一、基于通用http/html请求
大家都知道LoadRunner是一种性能测试工具,但它也可以用在我们做接口测试的时候。
(一)接口使用说明书要求
开发人员开发出来的接口,提供给测试人员详细的接口使用说明书,该说明书最基本的要求如下:
接口测试地址:/SNS/Publish
请求报文参数说明:
参数名称 | 参数描述 | 字符类型 | 字符值 |
SNSID | 社区ID | String | 6 |
UserID | 用户ID | String | 10 |
CommentsTypeID | 评论类型ID | String | 2 |
CommentsID | 评论ID | String | 10 |
AuthorID | 作者ID | String | 10 |
CommentsContent | 评论内容 | String | 50 |
请求报文格式:
<?xml version="1.0" encoding="ISO-8859-1"?> < Publish ><SNSID>123</SNSID><UserID>456</ UserID><CommentsTypeID>2</ CommentsTypeID><CommentsID>123</CommentsID><AuthorID>456</AuthorID><CommentsContent>Don't forget the meeting!</CommentsContent></Publish> |
应答报文的参数接口说明:
参数名称 | 参数描述 | 字符类型 | 字符值 |
UserID | 用户ID | String | 10 |
CommentsTypeID | 评论类型ID | String | 2 |
CommentsID | 评论ID | String | 10 |
CommentsContent | 评论内容 | String | 50 |
StatusCode | 返回值 | Int | 0代表pass,非0代表fail |
StatusText | 返回信息描述 | String |
<?xml version="1.0" encoding="ISO-8859-1"?> < Publish ><UserID>456</ UserID><CommentsTypeID>2</ CommentsTypeID><CommentsID>123</CommentsID><CommentsContent>Don't forget the meeting!</CommentsContent><StatusCode>0</StatusCode><StatusText>发送成功一条评论</StatusText></Publish> |
(二)测试方式
有了上述的说明书之后,测试人员可以根据文档的描述在LoadRunner书写相应的接口测试脚本。
LoadRunner中涉及到向服务器发送请求的API方法包括:web_url(),web_submit_form(),web_submit_data(),web_custom_request()。下面介绍两种我常用的方法:
1、方法一:使用web_submit_data()
web_submit_data("insert", "Action=http://116.211.23.123/SNS/Publish.htm ", "Method=POST", "Referer=http://116.211.23.123/SNS/Publish.htm ", "Mode=HTML", ITEMDATA, "Name= SNSID ","Value=6601",ENDITEM, "Name= UserID ","Value=123",ENDITEM, "Name= CommentsTypeID ","Value=1",ENDITEM, "Name= CommentsID ","Value=456",ENDITEM, "Name= AuthorID","Value=789",ENDITEM, "Name= CommentsContent ","Value=Just for testing",ENDITEM, LAST); |
2、方法二:使用web_custom_request()
char str[1000]; strcpy(str,"SNSID=7999&UserID=1&CommentsTypeID=1&CommentsID=1&AuthorID=1&CommentsContent=1");web_custom_request("Publish", "Url= http://116.211.23.123/SNS/Publish.htm", "Method=POST", "Referer=http://116.211.23.123/SNS/Publish.htm ", "Mode=HTTP", str, LAST); |
这也是一种写法,可以跟web_submit_data互换。这种写法更利于拼接参数。
方法一适合一些xml结构的根元素下的子元素同处于根元素下面,且子元素数目较少的情况下,如果xml结构比较复杂,比如说根元素下面有多级子元素,或者xml树结构分叉较多的时候,我们可以先把xml拼接成一个字符串然后通过web_custom_request()向服务器发送请求。
我们在做接口功能测试的时候会很注意接口的应答报文的信息,这时候我们可以通过LoadRunner的日志信息查看或者可以通过web_reg_find()或者web_find()这样的API函数来统计接口的运行结果,推荐使用web_reg_find(),web_reg_find()和web_find()区别请大家百度一下,详细信息太多,在这里不便叙述。
因为web_reg_find()是注册型函数,所以应该放在web_submit_data()或者web_custom_request()的前面。
如:
web_reg_find("Text=<StatusCode>0</StatusCode>",//应答报文里边的信息 "SaveCount= StatusCodeCount", //统计查询字段的信息,如果找到值为1,如果未找到值为0LAST); |
在脚本的最后我们可以对查询字段的信息进行统计
// Check result if (atoi(lr_eval_string("{StatusCodeCount }")) > 0){ //判断如果Welcome字符串出现次//数大于0lr_output_message("Send out the comment successfully."); }//在日志中输出Send out //the comment successfullyelse{ //如果出现次数小于等于lr_error_message("Send out the comment unsuccessfully."); //在日志中输出Send out //the comment successfullyreturn(0);} |
总结:用LoadRunner做接口测试无法做到把接口参数和程序分理,接口的参数可以通过参数化的方法来实现对同一个参数多个数据的测试。参数化后的测试数据保存在此脚本的保存位置下。
三、基于JSON格式的接口请求
(一)接口使用说明书要求
1、接口调用说明
网际协议:HTTP POST
POST内容类型:application/x-www-form-urlencoded或application/json
字符集:utf-8
POST 内容:jsonstr={"method":"接口名称", 接口名称参数}
例:jsonstr={
"c":"1001",
"p":{
"code":"9051",
"mobile":"{phone}",
"pwd":"123456",
"model":"{model}",
}
}
接口访问地址:http://{ip}:{port}/ei/mcu/MCUServiceEx
2、参数描述
(1)输入参数描述
输入格式 | 描述 |
json | 必填参数包括: l c:主体编号 l 其中p列表参数中的必填参数为: (json数组)code:主体编号;mobile:电话号;pwd:密码。 |
(2)返回参数描述
返回值 | 描述 |
json | 操作成功则返回: { "responseCode": 200, "errorCode": 0, "responseCode": "连接成功", "saveCodeMsg":"提交成功", "result": { "confId":"909" } } 网络连接失败返回: { "responseCode": 500 } 业务操作失败返回: { "responseCode": 200, "errorCode":错误码, "responseCode": "连接成功", "saveCodeMsg":"", "result": "" } |
(二)测试方式
1、通用方法
使用lr测试json接口,向服务端发送json格式请求,接收处理返回响应数据。
主要用到函数:
1)web_custon_request
2)web_reg_save_param (此函数常用户处理动态参数,该方法在LoadRunner中被称为Correlation(关联))
具体实例如下(包含发送json,获取响应数据,设置关联、设置检查点):
/*
定义事务:
1)用户注册
2)用户登录
3)用户签到
接口为:application/json
接口格式描述样例:
{ "c":"1001",
"p":{
"code":"9051",
"mobile":"{phone}",
"pwd":"123456",
"model":"{model}",
}
}
思路:
1)app注册,获到mobile
2)使用获取到的mobile登录,获取tokenid和userid。
参数化:手机号码、设备号
*/
Action()
{
//定义接口url
char *Interface_Url;
lr_save_string("http://192.168.0.1:8080/action.a","Interface_Url");
//注册事务开始
lr_start_transaction("用户注册");
lr_rendezvous("register");
//检查用户是否注册成功
web_reg_find("Search=Body",
"Text=createTime",
"SaveCount=createTime",
LAST);
//web_set_max_html_param_len("10000000");
//获取用户注册后响应数据,并传给mobile
web_reg_save_param("mobile",
"LB=\"mobile\":\"", //设置左边界
"RB=\",", //设置右边界
"Ord=1",
"Notfound=empty",
"Search=Body",
LAST);
web_custom_request("register",
"Url={Interface_Url}", //设置url
"Method=POST", //设置方法,post或get
"Mode=HTTP",
"EncType=application/json; charset=UTF-8 ", //编码类型
"RecContentType=application/json",
"Body={\"c\":\"1001\",\"p\":{\"code\":\"9051\",\"mobile\":\"{phone}\",\"pwd\":\"123456\",\"model\":\"{model}\",}}", //请求主体,json格式,双引号前需要加斜杠
LAST);
lr_vuser_status_message("注册事务,虚拟用户数#%s",lr_eval_string("{phone}"));
lr_end_transaction("用户注册",LR_AUTO);
//登录事务开始
lr_start_transaction("用户登录");
lr_rendezvous("login");
web_reg_find("Search=Body",
"Text=lastLoginTime",
"SaveCount=lastLoginTime",
LAST);
//获取用户登录后响应数据tokenId,并传给pdl_dynamic_tokenId
web_reg_save_param("pdl_dynamic_tokenId",
"LB=\"tokenId\":\"",
"RB=\"",
"Ord=1",
"Notfound=empty",
"Search=Body",
LAST);
//从用户登录响应数据中,获取pdl_user_id
web_reg_save_param("pdl_user_id",
"NotFound=warning",
"LB=\"id\":",
"RB=,\"inviteCode",
"Ord=1",
"Search=Body",
LAST);
web_custom_request("login",
"Url={Interface_Url}",
"Method=POST",
"Mode=HTTP",
"EncType=application/json",
"RecContentType=application/json",
"Body={\"c\":\"1002\",\"p\":{\"mobile\":\"{pdl_mobile}\",\"pwd\":\"123456\"}}",
LAST);
lr_end_transaction("用户登录",LR_AUTO);
//用户签到
lr_start_transaction("用户签到");
lr_rendezvous("Sign_in");
web_reg_find("Search=Body",
"Text=signDate",
"SaveCount=signDate",
LAST);
web_custom_request("pdl_Sign_in",
"Url={Interface_Url}",
"Method=POST",
"Mode=HTTP",
"EncType=application/json",
"RecContentType=application/json",
"Body={\"c\":\"1014\",\"p\":{\"userId\":\"{pdl_user_id}\",\"tokenId\":\"{pdl_dynamic_tokenId}\"}}",
LAST);
if(atoi(lr_eval_string("{response}"))>0){ //通过检查点判断请求返回是否成功
lr_end_transaction("用户签到", LR_PASS);
}else{
lr_end_transaction("用户签到", LR_FAIL);
}
return 0;
}
2、json数据参数化
在做接口测试时,可能使用到请求体式为json的数据,可以通过将json数据参数化,以便能方便的维护和管理接口请求数据,而且更容易格式化数据。
(1)在loadrunner中创建一个参数文件,填入需要请求的json串数据,如图所示。
(2)请求时注意服务器支持的编码格式,所有我先做一个数据读取并转换为UTF-8格式,这里有个地方要注意转成utf-8后要再进行参数存储
【lr_save_string(lr_eval_string("{tmpParam}"),"tmpParam"))】,因为转换后字符串结束会多出来“\x00”
(3)由于要进行json请求,所有必须在请求头指定内容类型为json,这个也是进行json请求的关键。
3、json数据带中文编码参数
Action()
{
charstrs[1000];
char*buffer; //使用指针最好.
buffer= (char*)malloc(1024); //记得在vuser_end中释放指针
//charstrJson[]="{\"EntName_3\":\"{EntName}\",\"RegNo_3\":\"\",\"SubDateStart_3\":\"\",\"SubDateEnd_3\":\"\",\"InsDate\":\"\",\"InsType\":\"\",\"InsRes\":\"\",\"DataProWay_3\":\"\",\"ErrCode_3\":\"\",\"cb_gridTable3\":\"0\",\"undefined\":\"10\",\"EntName\":\"\",\"RegNo\":\"\",\"SubDateStart\":\"\",\"SubDateEnd\":\"\",\"DataProWay\":\"\",\"ErrCode\":\"\",\"PageType\":\"3\"}";
//charstrs[200];//先定义不赋值,再赋值时可以将参数转为变量
//postData={"EntName_3":"","RegNo_3":"","SubDateStart_3":"","SubDateEnd_3":"","InsDate":"","InsType":"","InsRes":"","DataProWay_3":"","ErrCode_3":"","cb_gridTable3":"0","undefined":"10","EntName":"","RegNo":"","SubDateStart":"","SubDateEnd":"","DataProWay":"","ErrCode":"","PageType":"3"}&_search=false&nd=1484875494234&rows=10&page=1&sidx=&sord=asc
//方案一,整个Json串定为参数
lr_convert_string_encoding(lr_eval_string("{urlOptName}"),LR_ENC_SYSTEM_LOCALE,LR_ENC_UTF8 , "UnicodeStringOptName");
lr_save_string (lr_eval_string("{UnicodeStringOptName}"),"urlOptName"); //去掉结尾的NULL
web_convert_param("urlOptName","SourceEncoding=PLAIN", "TargetEncoding=URL",LAST );
lr_log_message("获取转码后的get Json方案一:%s",lr_eval_string("{urlOptName}"));
//----------------------------------------------------------------------
//方案二,JSON将参数内容串联,指针变量形式(低效率方案)
strcpy(buffer,"{\"EntName_3\":\"");
strcat(buffer,lr_eval_string("{EntName}"));
strcat(buffer,"\",\"RegNo_3\":\"\",\"SubDateStart_3\":\"\",\"SubDateEnd_3\":\"\",\"InsDate\":\"\",\"InsType\":\"\",\"InsRes\":\"\",\"DataProWay_3\":\"\",\"ErrCode_3\":\"\",\"cb_gridTable3\":\"0\",\"undefined\":\"10\",\"EntName\":\"\",\"RegNo\":\"\",\"SubDateStart\":\"\",\"SubDateEnd\":\"\",\"DataProWay\":\"\",\"ErrCode\":\"\",\"PageType\":\"3\"}");
lr_convert_string_encoding(buffer,LR_ENC_SYSTEM_LOCALE,LR_ENC_UTF8 , "UnicodeStringOptName2");
lr_save_string(lr_eval_string("{UnicodeStringOptName2}"),"urlOptName2" );//去掉结尾的NULL
web_convert_param("urlOptName2","SourceEncoding=PLAIN", "TargetEncoding=URL",LAST );
lr_log_message("获取转码后的get Json方案二:%s",lr_eval_string("{urlOptName2}"));
strcpy(buffer,lr_eval_string("{urlOptName2}"));
strcat(buffer,"&_search=false&nd=1484875494234&rows=10&page=1&sidx=&sord=asc");
lr_log_message("完整GET请求串:%s",buffer);
//方案三,JSON整体内容部分参数化,指针变量形式(最佳方案)
buffer="{\"EntName_3\":\"{EntName}\",\"RegNo_3\":\"\",\"SubDateStart_3\":\"\",\"SubDateEnd_3\":\"\",\"InsDate\":\"\",\"InsType\":\"\",\"InsRes\":\"\",\"DataProWay_3\":\"\",\"ErrCode_3\":\"\",\"cb_gridTable3\":\"0\",\"undefined\":\"10\",\"EntName\":\"\",\"RegNo\":\"\",\"SubDateStart\":\"\",\"SubDateEnd\":\"\",\"DataProWay\":\"\",\"ErrCode\":\"\",\"PageType\":\"3\"}";
lr_convert_string_encoding(lr_eval_string(buffer),LR_ENC_SYSTEM_LOCALE,LR_ENC_UTF8 , "UnicodeStringOptName3");
lr_save_string(lr_eval_string("{UnicodeStringOptName3}"),"urlOptName3" );//去掉结尾的NULL
web_convert_param("urlOptName3","SourceEncoding=PLAIN", "TargetEncoding=URL",LAST );
lr_log_message("获取转码后的get Json方案三:%s",lr_eval_string("{urlOptName3}"));
free(buffer );//可以放到vuser_end
buffer= NULL;//可以放到vuser_end
//方案四,JSON数据部分参数化,数组变量形式(普遍方案)
strcpy(strs,"{\"EntName_3\":\"{EntName}\",\"RegNo_3\":\"\",\"SubDateStart_3\":\"\",\"SubDateEnd_3\":\"\",\"InsDate\":\"\",\"InsType\":\"\",\"InsRes\":\"\",\"DataProWay_3\":\"\",\"ErrCode_3\":\"\",\"cb_gridTable3\":\"0\",\"undefined\":\"10\",\"EntName\":\"\",\"RegNo\":\"\",\"SubDateStart\":\"\",\"SubDateEnd\":\"\",\"DataProWay\":\"\",\"ErrCode\":\"\",\"PageType\":\"3\"}");
lr_convert_string_encoding(lr_eval_string(strs),LR_ENC_SYSTEM_LOCALE,LR_ENC_UTF8 , "UnicodeStringOptName4");
lr_save_string (lr_eval_string("{UnicodeStringOptName4}"),"urlOptName4"); //去掉结尾的NULL
web_convert_param("urlOptName4","SourceEncoding=PLAIN", "TargetEncoding=URL",LAST );
lr_log_message("获取转码后的get Json方案四:%s",lr_eval_string("{urlOptName4}"));
memset(strs,0,sizeof(strs));//清空内存*/
//方案五,Json部分内容替换为中文
/*ReplaceStr(strJson,"{EntName}","吉林省锐迅股份");//ReplaceStr是在globals.h中创建的自定义函数
lr_convert_string_encoding(strJson,LR_ENC_SYSTEM_LOCALE, LR_ENC_UTF8 ,"UnicodeStringOptName3");
lr_save_string(lr_eval_string("{UnicodeStringOptName3}"),"urlOptName3" );//去掉结尾的NULL
web_convert_param("urlOptName3","SourceEncoding=PLAIN", "TargetEncoding=URL",LAST );
lr_log_message("获取转码后的get Json方案五:%s",lr_eval_string("{urlOptName3}"));
memset(strJson,0,sizeof(strJson));//释放内存*/
return0;
}
二、基于WebService以及Soap接口的测试
(一)接口使用说明书要求
1、调用方法:
GiftCardQueryService(string userId,bool userIdSpecified,int days,bool daysSpecified,stringvalidateValue)
2、调用地址:http://{IP}:{port}/WebServices/RandomFontsWebService.asmx?wsdl
3、输入参数
参数名称 | 说明 |
userId | 用户ID |
userIdSpecified | |
days | |
daysSpecified | |
validateValue |
4、返回值
参数名称 | 说明 |
GetValidGiftCardNumForDayResult |
(二)测试方式
1、通过web_service_call函数
具体可以参见网上的相关资料
启动LR选择 Web Service协议,并添加调用服务,设置好传入参数及返回值,LR自带函数web_service_call就封装好了本次传参及返回结果集,举例如下:
web_service_call("StepName=GetValidGiftCardNumForDay_101", "SOAPMethod=GiftCardQueryService|BasicHttpBinding_GiftCardQueryService|GetValidGiftCardNumForDay",
"ResponseParam=response",
"Service=GiftCardQueryService",
"ExpectedResponse=SoapResult",
"Snapshot=t1340760446.inf",
BEGIN_ARGUMENTS,
"userId={userid}",
"userIdSpecified=true",
"days=10",
"daysSpecified=true",
"validateValue={key}",
END_ARGUMENTS,
BEGIN_RESULT,
"GetValidGiftCardNumForDayResult=Param_GetValidGiftCardNumForDayResult",
END_RESULT,
LAST);
2、通过soap_reques发送报文主体
Action()
{
int count;
lr_start_transaction("oneSoapOrder");
web_add_header("SOAPAction","");
soap_request("StepName=createSoapOrder",
"URL=http://192.168.101.1:8080/MySoapProj/services/SoapService",
"SOAPEnvelope=<?xmlversion=\"1.0\" encoding=\"GB2312\"?>"
"<soapenv:Envelopexmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">"
"<soapenv:Header>"
"<Security>"
"<UsernameToken>"
"<Username>username</Username>"
"<Password>password</Password>"
"</UsernameToken>"
"</Security>"
"</soapenv:Header>"
"<soapenv:Body>"
"<msg:createSoapOrder>"
"<msg:SoapOrder>"
"<id>987600001</id>"
"</msg:businessGroup>"
"</msg:createSoapOrder>"
"</soapenv:Body></soapenv:Envelope>",
"ResponseParam=response",
LAST);
count =lr_xml_find("XML={response}",
"Query=/Envelope/Body/createSoapOrderResponse/resultcode/value",
"Value=0",
"NotFound=continue",
LAST);
if(count > 0){
lr_end_transaction("oneSoapOrder",LR_PASS);
}
else{
lr_end_transaction("oneSoapOrder",LR_FAIL);
}
return 0;
}
这里需要注意:
1)报文中节点的域名,最好不要变动;
2)SOAP的URL需要根据实际情况修改;
3)在一个方法中的某个参数,如果太长,需要换行,则每行单独用双引号括起来;
4)lr_start_transaction("oneSoapOrder");方法,是创建一个名为oneSoapOrder的事务。该事务的结束,由lr_end_sub_transaction方法实现;
5)soap_request方法中的"ResponseParam=response"参数,是将SOAP返回报文存放到response中;
6)lr_xml_find方法是查找xml文件中某个指定节点的值。此例中,XML文件存放在response的SOAP响应报文,查找的节点是/Envelope/Body/createSoapOrderResponse/resultcode/value节点(具体的响应返回路径请根据实际项目替换),即返回码。预期结果"Value=0"。该方法如果找到预期值,则返回找到的预期值个数;找不到的话,则返回0;
7)根据count的值,判断返回码是否是0(操作成功),从而决定事务是否成功。(LR_PASS表示事务成功;LR_FAIL表示事务失败);
8)这个报文是静态的,再多的虚拟用户执行,也只是执行同一条指令。若想使参数能“动”起来(如编号等取值不冲突的参数)。