Testing Web Services With a Standard Web Vuser
October 6th, 2008 Posted by Stuart Moncrieff 6 Comments »
It is possible to test web services using the standard Web (HTTP/HTML) virtual user type instead of the Web Services vuser type. The main disadvantage of this is that you cannot generate your SOAP body from the WSDL file using the VuGen wizard. But if you know what your XML request should look like, then you shouldn’t have any real problems.
Here are my tips:
• Send your SOAP payload using lr_custom_request().
• Add a SOAPAction HTTP header using web_add_header().
• Remove unnecessary HTTP headers (that are generated automatically by VuGen) with web_remove_auto_header().
• Don’t forget to verify that you get a valid response. Use web_reg_find() for a simple check. For better verification of the SOAP response use lr_xml_find().
• To extract values from the response body, use lr_xml_get_values(). Brush up on your XPath qeries beforehand though.
• It may be necessary to HTML-encode some characters in your XML/SOAP message (e.g. convert “&” to “&”). Unfortunately VuGen does not provide this functionality (but HP could easily add it to the web_convert_param function), so you will have to either write (or find) a function to do it, or convert all the entries in your data table before running the script.
As an example, here is a simple script that makes use of a web service that will look up the source of a Shakespeare quote for you. The WSDL is available from http://www.xmlme.com/WSShakespeare.asmx?wsdl.
Action()
{
// ContentCheck Rules for known error messages
web_global_verification("Text=Speech not found", "ID=SpeechNotFound", LAST);
lr_start_transaction ("Search For Shakespeare Quote");
// By default, VuGen sends a user-agent header.
// Let's remove this as an example of removing automatically generated headers.
web_remove_auto_header("User-Agent", "ImplicitGen=No", LAST);
// Add a SOAPAction HTTP header
web_add_header("SOAPAction", "http://xmlme.com/WebServices/GetSpeech");
// Save entire body from the HTTP response for later checking with lr_xml_find.
web_reg_save_param("ResponseBody",
"LB=",
"RB=",
"Search=Body",
"IgnoreRedirections=Yes",
LAST);
// Note that the text to search for would normally be replaced with a parameter,
// and so would the
web_reg_find("Text=TWELFTH NIGHT", LAST);
web_custom_request("Search Shakespeare",
"URL=http://www.xmlme.com/WSShakespeare.asmx",
"Method=POST",
"Resource=0",
"Referer=",
"Snapshot=t1.inf",
"Mode=URL",
"EncType=text/xml; charset=utf-8",
"Body=" // As it is SOAP, you are unlikely to have to use BodyBinary, unless your request has CDATA.
""
"
"
"
"
"
"
"
LAST);
// The response from the web service looks like this:
/*
<SPEECH>
<PLAY>TWELFTH NIGHT</PLAY>
<SPEAKER>MALVOLIO</SPEAKER>
'Be not afraid of greatness:' 'twas well writ.</SPEECH>
*/
// An example of extracting the a value from a SOAP reponse.
// This saves the
// The same syntax could be used with lr_xml_find to check the response.
lr_xml_extract("XML={ResponseBody}",
"XMLFragmentParam=OutputParameter",
"Query=/soap:Envelope/soap:Body/GetSpeechResponse/GetSpeechResult", LAST);
lr_output_message("Source of Shakespeare quote: %s", lr_eval_string("{OutputParameter}"));
lr_end_transaction ("Search For Shakespeare Quote", LR_AUTO);
return 0;
}
Writing to a file
/*
Writes a string to the end of a file.
Arguments:
- file_name: Include the full path in the file name, and escape any slashes. E.g. "C:\\TEMP\\output.txt". Note that file does not have to exist beforehand, but directory does.
- string: If attempting to write a single line, include a newline character at the end of the string.
Returns 0 on success. On failure, function will raise lr_error_message and return -1.
*/
int jds_append_to_file(char* file_name, char* string) {
int fp; // file pointer
int rc; // return code
int length = strlen(string);
// Check that file_name is not NULL.
if (file_name == NULL) {
lr_error_message("Error. File name is NULL");
return -1;
}
fp = fopen(file_name, "a"); // open file in "append" mode.
if (fp == NULL) {
lr_error_message("Error opening file: %s", file_name);
return -1;
}
rc = fprintf(fp, "%s", string);
if (rc != length) {
lr_error_message("Error writing to file: %s", file_name);
return -1;
}
rc = fclose(fp);
if (rc != 0) {
lr_error_message("Error closing file: %s", file_name);
return -1;
}
return 0;
}
Check if a file already exists
// Checks if a file already exists on the filesystem.
// Arguments:
// - file_name: Include the full path in the file name.
// Returns TRUE (1) if file exists and user has read access to the file, otherwise function returns FALSE (0).
int jds_file_exists(char* file_name) {
int fp; // file pointer
fp = fopen(file_name, "r+"); // open file in read mode. File must already exist.
if (fp == NULL) {
return FALSE;
} else {
fclose(fp);
return TRUE;
}
}
Saving a file to hard disk
// Saves a file to the hard disk.
// Arguments:
// - file_name: Include the full path in the file name. Note that file must not exist before function is called.
// - file_content: The data to save to the file. Can be binary or string data.
// - file_size: The size/length of the data to save to the file. If it is string data, you can find this using strlen(). If you are saving binary data from a web page, use web_get_int_property(HTTP_INFO_DOWNLOAD_SIZE).
// Returns 0 on success. On failure, function will raise lr_error_message and return -1.
int jds_save_file(char* file_name, void* file_content, unsigned int file_size) {
int rc; // function return code
int fp; // file pointer
// Check input values
if (file_name == NULL) {
lr_error_message("File name is NULL");
return -1;
} else if (file_content == NULL) {
lr_error_message("File content is NULL");
return -1;
} else if (file_size < 1) { lr_error_message("Invalid file size: %d", file_size); return -1; } // Does the file already exist? if (jds_file_exists(file_name) == TRUE) { lr_error_message("File %s already exists", file_name); return -1; } fp = fopen(file_name, "wb"); // open file in "write, binary" mode. if (fp == NULL) { lr_error_message("Error opening file: %s", file_name); return -1; } rc = fwrite(file_content, file_size, 1, fp); if (rc != 1) { lr_error_message("Error writing to file. Items written: %d", rc); return -1; } rc = fclose(fp); if (rc != 0) { lr_error_message("Error closing file: %s", file_name); return -1; } return 0; } Saving a binary file from a webpage (like a PDF or a GIF). Note that this is not a good way to veryify that your LoadRunner/BPM script is running successfully. Action() { int size; char* file = "C:\\TEMP\\test.zip"; // Make this big enough to hold the downloaded file. web_set_max_html_param_len("1048576"); // 1 MB // Save entire HTTP response body web_reg_save_param("FileContents", "LB/BIN=", "RB/BIN=", "Search=Body", LAST); // Note that it is best to use web_custom_request, as this guarantees that only one file is being downloaded by this step. web_custom_request("DownloadPlugin", "URL=http://www.example.com/files/test.zip", "Method=GET", "Resource=1", "RecContentType=text/css", "Referer=http://www.jds.net.au", "Snapshot=t1.inf", LAST); // returns the size of the previous HTTP response size = web_get_int_property(HTTP_INFO_DOWNLOAD_SIZE); jds_save_file(file, lr_eval_string("{FileContents}"), size); return 0; } Understanding Aggregate Variance within LoadRunner Analysis January 23rd, 2009 Posted by Nick Wilton No Comments » From time to time, you may notice variances within the Loadrunner Analysis tool. This is most apparent when reviewing the Transaction Response Time (Percentile) graph. If you look at the following Transaction Summary Report: And look at the following Transaction Response Time (Percentile) graph: As you can see there are some conflicting values: Summary Report Graph Legend Graph Mouseover (Tooltip) Average 50th Percentile Average Median 50th Percentile 7.718 5.728 7.942 5.830 5.728 Firstly, I’ll state that all these values are correct. The LoadRunner Analysis engine calculates all values correctly, and all variances can be easily explained by examining the data. Median vs Mean vs Average vs Percentile The first concept to understand and permanently implant into your brain, is the difference between these four aggregate types. An “Aggregate” is a single value that represents an underlying set of multiple values, commonly known aggregates are Average, Minimum and Maximum. Most people understand what an “Average” is…that is the sum of a value set divided by the count of the value set. “Average” is also known as “Mean”. “Median” however is a different concept, a median value is calculated by sorting a set of data values from smallest to largest value then dividing this sorted set by two. The “Median” is the middle value at the place you divided the set. Median is identical to the 50th Percentile. Percentiles are an extension on the median concept, but instead of dividing the sorted value set by two…we instead divide the value set by a percentage (starting with the smallest value). So the 15th Percentile, refers to the value 15% along the sorted set…the 75th Percentile refers to the value 75% along the sorted set. Also the 0th Percentile is undefinable value. Based on this information, we can safely separate the values into the following two groups. Average Median/ 95th Percentile Summary Report 7.718 5.728 Graph Legend 7.942 5.830 Graph Mouseover - 5.728 Clearly the median value for the Graph Mouseover and the Summary Report are identical, so for the rest of the article I’ll will no longer refer to the Graph Mouseover. Graph Values vs Raw Values When the Percentile graph is generated (or any other graph for that matter), the analysis tool generates a small sample set for the graph. The sample set size is configurable in some graphs by setting the graph’s granularity, however the Percentile graph is fixed at a sample set of 100 data values…with each corresponding to a separate percentile value. Therefore the values for Average and Median for the Graph Legend are calculated from the Graph data, not the Raw data. Keeping this in mind, the Graph Average value is simply the average of the 1th, 2nd, 3rd…100th Percentile. Whilst this is graph average is close, it is not as accurate as the Summary Report average (which is calculated from the complete raw data set). Okay, so up to this point we’ve explained all the value variations, except for the difference with the Median value. This problem is interesting, remember I mentioned that the median value is simply the sorted value set divided into two…and that the 0th Percentile is undefined. Well, the analysis tool equally divides the graph data result set into a set of values representing the 1th Percentile through to the 50th Percentile, and the 51st Percentile through to the 100th Percentile. As there is no clearly defined midpoint in this data set the analysis engine uses the next value…in this case the 51st Percentile. So in summary. the Graph Median value actually the value of the 51st Percentile…not the 50th Percentile. This may be considered a minor bug. In Summary In summary, the graph value data is adequate for producing a graph…however for aggregate values I recommend only using the Summary Report data as the Summary report is the only report in the analysis engine that is guaranteed to display accurate data based on the complete data set. // Code taken from HP whitepaper on LoadRunner by // Opral Wisham from Waste Management, Inc. // This example was used in a script that required clicking // the "refresh" button until the run status changed to // "success". This code provides an automatic refresh // until the batch job has been completed. The next step // requires the completion of the batch job. int x; // flag will be 0 or 9 char *temp, *temp2; // values to hold strings Action() { temp2="Success"; //compare string 2 //lr_message("temp2 = %s", temp2); // set x to 0. x is the success flag x=0; do { web_reg_save_param("RunStatus", "LB=
"RB=\n",
"Ord=5",
"Search=body",
LAST);
web_submit_data("PROCESSMONITOR.PROCESSMONITOR.GBL",
"Action=http://crpu028a:8050/psc/fs84cpv/EMPLOYEE/ERP/c/PROCESSMONITOR.PROCESSMONITOR.GBL",
"Method=POST",
"RecContentType=text/html",
"Referer=http://crpu028a:8050/psc/fs84cpv/EMPLOYEE/ERP/c/PROCESSMONITOR.PROCESSMONITOR.GBL?Page=PMN_PRCSLIST&Action=U&",
"Snapshot=t17.inf",
"Mode=NORESOURCE",
ITEMDATA,
"Name=ICType", "Value=Panel", ENDITEM,
"Name=ICElementNum", "Value=0", ENDITEM,
"Name=ICStateNum", "Value={ICStateNum6}", ENDITEM,
"Name=ICAction", "Value=REFRESH_BTN", ENDITEM,
"Name=ICXPos", "Value=0", ENDITEM,
"Name=ICYPos", "Value=0", ENDITEM,
"Name=ICFocus", "Value=", ENDITEM,
"Name=ICChanged", "Value=-1", ENDITEM,
"Name=ICFind", "Value=", ENDITEM,
"Name=PMN_FILTER_WS_OPRID", "Value=CPVID", ENDITEM,
"Name=PMN_FILTER_PRCSTYPE", "Value=", ENDITEM,
"Name=PMN_FILTER_FILTERVALUE", "Value=1", ENDITEM,
"Name=PMN_FILTER_FILTERUNIT", "Value=1", ENDITEM,
"Name=PMN_FILTER_SERVERNAME", "Value=PSUNX", ENDITEM,
"Name=PMN_FILTER_PRCSNAME", "Value=", ENDITEM,
"Name=PMN_DERIVED_PRCSINSTANCE", "Value={Process_Instance}", ENDITEM,
"Name=PMN_DERIVED_TO_PRCSINSTANCE", "Value={Process_Instance}", ENDITEM,
"Name=PMN_FILTER_RUNSTATUS", "Value=", ENDITEM,
LAST);
// Compare correlation value with character string
temp = lr_eval_string(lr_eval_string("{RunStatus}"));
// correlation value to variable
//lr_message("temp = %s", temp);
//compare string 1
if(strcmp(temp,temp2)==0){
// string compare success with correlation value
x=9; // set flag to indicate success
}
} while (x == 0); // do while flag not set
return 0;
}
Running a batch file using load runner
Action()
char command[1024]; // declare a cahar val
sprintf(command, "C:\\test\\test.bat");// mention the path of the file with full path details
system(command); // it will execute the command
return 0;
/**************************************/
on test.bat file i kept folowing command.
/************************************/
ping localhost -n 10
/***********************************/
now this batch file will run 10 times
Get the body length on a HTML request - This thread has been closed
Hi all,
I'm sending a request to an HTTP server which should return a PDF file. The request is:
web_url("sefas", "URL=http://localhost/test",
"TargetFrame=",
"Resource=1",
"RecContentType=application/pdf",
"Referer=",
"Snapshot=t2.inf",
LAST);
The response header don't have a content length defined, but on the replay log at the end of the web_url() function, I'll have this message:
Action.c(6): web_url("sefas") was successful, 62402 body bytes, 184 header bytes, 69 chunking overhead bytes [MsgId: MMSG-26385]
It gives the body length of the request, how can we get this value ?
thanks everyone.
Finally I found the solution (I did not find it in the first help look-up :(( ).
use the web_get_int_property() function.
It accept parameter as input, and one is HTTP_INFO_DOWNLOAD_SIZE and retrieve the complete size of the request (body+header+chunked).
if it could help someone.
Does Response time Include First Buffer time - This thread has been closed
Hi All,
This may be basic but I want to understand if the response time includes First buffer time also. Why I am asking this questions is average response time for one transaction 3.38 secs and the Page breakdown graph displayed an average of 14.8 secs for the transaction , out of which the first buffer is 13 secs. Can any one give me an idea on this. For the data what i thought is that I should not relate Response times with Page breakdown data. Please let me know your thoughts.
Subscribe to:
Post Comments (Atom)
Thanks for the tip. I was trying to get the length of a chunked binary response and it wouldn't work when doing a reg_save_param!
ReplyDelete