Rational Robot 可被用來對包含數據關聯的復雜Web應用進行 性能測試 。這里所謂數據關聯,是指Web頁面之間存在的數據相關性,例如一個動態的頁面URL或者個別輸入參數需要從前一個頁面中抽取出來,有時候還需要在抽取得到的結果的基礎上做進一步處理。這就使
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的典型用例
clearcase/" target="_blank" >cccccc border=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的“動態數據關聯”功能
如前所述,關聯的動態數據可以有四種形式提交給服務器,下面介紹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控件時,客戶端的模擬就變得更為重要,因為它們常會被用來根據某一邏輯響應用戶的操作在客戶端動態地生成一些數據的值??蛻舳四M的最直接有效的方法就是用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腳本。本文在分析了常見形式的數據關聯的基礎上介紹了其相應的處理方法。