使用Rational Robot測試含有數據關聯的Web應用

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頭參數、CookieURL參數和FORM參數。由於URL參數可被認爲是URL的一部分,因此可以認爲有四種可能發生關聯的動態數據:HTTP頭參數、CookieURLFORM參數。在RobotVU語言中,一個HTTP請求是通過調用庫函數“http_request”發出的,列表1是給出了一個典型的用例。請注意列表1中各粗體部分,它們分別代表了四種可能發生關聯的動態數據的形式中的一種。


列表1. 函數http_request的典型用例

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)的數據是不一樣的。後者基本上是爲了模仿最終用戶的輸入,它的值可以在腳本運行前確定並加載到數據池中(例如用戶名和密碼)。而本文中所指的動態數據大多是由服務器在運行時間生成和返回並需要在後續請求中以某種形式發送回去。不過,在某些非常特殊的情形下,例如當服務器對某個動態數據只生成一個有限集合的值(例如truefalse)並且對用戶會話不敏感,那麼可以使用一個加載了所有這些可能值的數據池用於該動態數據的賦值,前提是能與服務器生成這些值類似的邏輯來從數據池中獲得這些值(例如隨機方式)。

 
 
使用Robot動態數據關聯功能
如前所述,關聯的動態數據可以有四種形式提交給服務器,下面介紹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。對於瀏覽器存儲的CookieRobot會在錄製一個HTTP會話前查詢瀏覽器存儲的所有Cookie並將它們放在最後生成的腳本的COOKIE_CACHE部分中。Robot會把這些Cookie的過期日期設置爲足夠遠的將來以保證在腳本運行的時候它們不會過期。當腳本被回放時,COOKIE_CACHE中的Cookie都會被加載到內存中使得回放過程儘量符合實際情況。整個過程都由Robot自動完成,不需測試開發人員干預。對於動態Cookie,即那些在錄製腳本時由服務器返回的CookieRobot會將它們作爲HTTP請求頭的一部分保存在生成的腳本中(見列表1),但是在腳本被回放時它們的值會被替換爲服務器實際返回的值——如果服務器確實有返回的話,否則就使用腳本中記錄的值。
從動態性的角度考慮,一個URL可以被分割爲兩個部分:出現在“?”前的location部分,和出現在“?”後的可選的search部分。後者用於攜帶若干個URL參數因此比前者更具動態性。由於URL參數和FORM參數的處理非常類似,將下一小節中一起討論。對於相對比較靜態的location部分,Robot一般只簡單地保存錄制到的值而不會做任何處理。但不幸的是在實際情況中URLlocation部分也可能是動態地生成的,一個很好的例子就是WebSphere Portal Server生成的URL鏈接,在這種情況下就需要通過編程定製的字符串抽取來獲得。
FORM參數
很多Web應用並不區分FORM 參數和URL參數。實際上,當一個FORMGET方法提交的時候,它的參數就變爲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樣例

<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>



列表 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_DATAHTTP_HREF_DATAHTTP_COOKIE_DATA,分別代表FORM數據、URL數據或Cookie數據。每一個nametypetag的組合都唯一地確定了一個單一的值,調用http_find_values時最多可以指定21個這樣的組合。宏CHECK_FIND_RESULT驗證它返回的值不爲空,若爲空則提供一個缺省值,該缺省值是在腳本錄製時記錄的值。
可以發現,雖然使用了動態數據關聯技術,Robot還是隻能從FORM數據、URL數據或者Cookie數據中抽取參數值。如果動態數據的值被包含在其他地方,例如FORM中的“action”屬性中,Robot就無能爲力了。
 
URLlocation部分是由服務器動態生成或者部分參數不在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返回一個空字符串。


列表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 "<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”


列表 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 "<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”)來得到。這些函數也有相應的支持正則表達式的版本。


列表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的內容粘貼進去。在其他腳本中,只要在文件頭中添加下面這一行就可以使用上面介紹的函數了:

#include “newscript.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 AppletActiveX控件時,客戶端的模擬就變得更爲重要,因爲它們常會被用來根據某一邏輯響應用戶的操作在客戶端動態地生成一些數據的值。客戶端模擬的最直接有效的方法就是用VU語言實現由網頁中的JavaScript或者嵌入式組件所實現的數據構造過程。但在這之前,通常需要先抽取構造所需的輸入,上一節中介紹的函數會有助於此。舉個例子,列表8中的HTML頁面片斷使用JavaScript來根據一個員工的名字動態地生成一個編碼後的URL,列表9中的VU腳本片斷模擬了這一URL的構造過程。


列表8. 包含JavaScript代碼的HTML片斷樣例

<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>



列表9. 模擬JavaScriptVU腳本片斷樣例

#include "routines.s"
{
         string employeeName;
         employeeName = getStringByBoundaries(_response, "javascript:gotoPage('", "'");
         employeeName += ".htm";
         employeeName = http_url_encode(employeeName);
}
Web應用中的連續頁面存在數據關聯是很普遍的現象。使用Rational Robot通過或多或少的人工干預可以正確地處理這些關聯從而產生更完善的VU腳本。本文在分析了常見形式的數據關聯的基礎上介紹了其相應的處理方法。
 
發佈了43 篇原創文章 · 獲贊 3 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章