欢迎光临51testing软件测试网,网站有任何问题,请与我联系

AJAX测试的自动化

上一篇 / 下一篇  2007-05-17 17:40:24 / 个人分类:Web测试

  在过去一年內有愈来愈多采用 AJAX (Asynchronous Javascrīpt And XML) 技术的 Web 应用程式。相对于非 AJAX Web 应用程式,正确撰寫的 AJAX 可以明显改善效能和使用者的使用经验。然而,由于 AJAX Web 应用程式是以非同步的方式运作,所以传统的同步测试自动化技术通常不适用。在本月的专栏中,我提出一项技术,这可让您撰写轻量型测试自动化程式,来验证 AJAX Web 应用程式的功能。

  首先让我用几个撷取画面,向您说明本专栏的主轴。[图 1]显示一个采用 AJAX 技术的简单、典型的 ASP.NET Web 应用程式。此应用程式会模拟一种地图工具,例如 Microsoft®Windows Live™ Local。当使用者按一下其中一个方向性按钮控制项时,应用程式会从 Web 私服器撷取适当的地图影像,然后将新的影像显示在中心区域。从撷取画面上无法明显看出的是,Web 应用程式乃使用 Microsoft ASP.NET AJAX 来非同步地传送和接收地图影像。当然,您的 Web 应用程式会更复杂,但我说明的技术很容易运用在任何复杂的应用程式上,也适用于任何 AJAX 应用程式,且适用于所有实作技术。

若以手动的方式测试 [图 1] 的应用程式,不但会很费时、无效率且单调乏味。较好的测试方法,是撰写轻量型测试自动化程式,如 [图 2] 所示。测试控管是一个双框架的简单 HTML 页面。右边框架会转载 Web 应用程式,且不含任何修改或测量。左边框架則会转载单一 HTML 页面,內含 Javascrīpt 程式码,以 Internet Explorer® 文件物件模型 (DOM) 来操作另一个框架中的 Web 应用程式。

虽然此测试技术是專为处理使用非同步作业的应用程式而設計,但一樣适用于使用传统同步 HTTP 要求/回应机制的 Web 应用程式。在本专栏中,我会简短说明测试中的 Web 应用程式,让您瞭解为何传统的测试自动化技术在 AJAX 应用程式上只得较无效率。接着,我会详細解释产生 [图 2] 影像的测试控管,也会说明如何修改和延伸此处提及的技术,来慢足您的需求。到最后,我相信您会发现这项技术可以让开发人员和测试人员的技能更上一层楼。

图1 测试中的AJAXWeb应用程式(按影像可放大)

测试中的Web应用程式

我利用 ASP.NET AJAX 程式碼程式庫建立如[图 1]所示的 AJAX 地图应用程式,因为此程式庫可大幅简化建立 AJAX 应用程式的工作。我利用ASP.NETAJAX程式码程式库建立如[图 1]所示的AJAX地图应用程式,因为此程式库可大幅简化建立AJAX应用程式的工作。 我並未使用真正的地图影像,只是使用九張含有數字 1 到 9 的假影像,依序命名为 1.jpg 到 9.jpg (请参阅 Jeff Prosise 在本期的 Wicked Code 专栏中,使用 Microsoft TerraServer 來取得真正地图影像的範例)。我并未使用真正的地图影像,只是使用九张含有数字1到9的假影像,依序命名为1.jpg到9.jpg(请参阅JeffProsise在本期的WickedCode专栏中,使用MicrosoftTerraServer来取得真正地图影象的范例)。 我的縮小版世界是由一個 3x3 的方格組成,第一列有 1、2、3,第二列有 4、5、6,第三列有 7、8、9。5 是世界的中心,如果使用者從 5 按一下 [往北] 控制按鈕,应用程式會顯示 2。我的缩小版世界是由一个3x3的方格组成,第一列有1、2、3,第二列有4、5、6,第三列有7、8、9。5是世界的中心,如果使用者从5按一下[往北]控制按钮,应用程式会显示2。 [图 3]顯示 [往北] 按鈕的逻辑。[图 3]显示[往北]按钮的逻辑。


特別說明一下,為了让应用程式码看起來簡單明瞭,我在這裡故意不用标准程式撰寫慣例。特别说明一下,为了让应用程式码看起来简单明了,我在这里故意不用标准程式撰写惯例。 在实际的 Web 应用程式中,您的程式码逻辑可能需要存取后端的 SQL 或 XML 存放區並擷取資料,然后以這項資料來更新应用程式狀態。在实际的Web应用程式中,您的程式码逻辑可能需要存取后端的SQL或XML存放区并撷取资料,然后以这项资料来更新应用程式状态。 因為本专栏讨论的技术是透过 Internet Explorer DOM 來測試 Web 应用程式,所以不在乎如何決定应用程式的狀態 - 任何使用者輸入都會導致应用程式的狀態改变,這會反映在应用程式的 UI 中,且您可以透过 Internet Explorer DOM 來取得狀態。因为本专栏讨论的技术是透过InternetExplorerDOM来测试Web应用程式,所以不在乎如何决定应用程式的状态-任何使用者输入都会导致应用程式的状态改变,这会反映在应用程式的UI中,且您可以透过InternetExplorerDOM来取得状态。

傳統的非 AJAX 方法會經由 Form 物件或查詢字串來傳送 HTTP 要求資訊,將要求傳遞至 Web 伺服器。传统的非AJAX方法会经由Form物件或查询字串来传送HTTP要求资讯,将要求传递至Web伺服器。 就算不使用 AJAX 技术,地图应用程式一樣可以執行,只是有兩個缺點。就算不使用AJAX技术,地图应用程式一样可以执行,只是有两个缺点。 首先,因為 HTTP 要求/回应是同步機制,所以當 Web 伺服器正在处理要求時,使用者必須等到回应傳回給用戶端,才能夠再與 Web 应用程式互動。首先,因为HTTP要求/回应是同步机制,所以当Web伺服器正在处理要求时,使用者必须等到回应传回给用户端,才能够再与Web应用程式互动。 其次,在大多數情況下,HTTP 要求會導致整個回应頁面的建立,所以當 Internet Explorer 收到回应時,將會重新繪製整個頁面。其次,在大多数情况下,HTTP要求会导致整个回应页面的建立,所以当InternetExplorer收到回应时,将会重新绘制整个页面。 如果要求/回应速度快,這將出現令人討厭的頁面閃爍現象,更糟的是,如果要求/回应速度太慢,甚至會出現空白頁面的現象。如果要求/回应速度快,这将出现令人讨厌的页面闪烁现象,更糟的是,如果要求/回应速度太慢,甚至会出现空白页面的现象。

AJAX 可以同時解決這兩個問題。AJAX可以同时解决这两个问题。 AJAX 會傳送 XMLHTTP 要求,而不是 HTTP 要求。AJAX会传送XMLHTTP要求,而不是HTTP要求。 XMLHTTP 要求為非同步性質。XMLHTTP要求为非同步性质。 在幕后处理 XMLHTTP 要求時,使用者仍然可以繼續與 Web 应用程式互動。在幕后处理XMLHTTP要求时,使用者仍然可以继续与Web应用程式互动。 在收到 XMLHTTP 回应時,Internet Explorer DOM 可以只重新繪製含有新資料的網頁區域,用不著重新繪製整個頁面。在收到XMLHTTP回应时,InternetExplorerDOM可以只重新绘制含有新资料的网页区域,用不着重新绘制整个页面。

在我的 ASP.NET 地图应用程式中,我並未撰寫「原始」Javascrīpt 來啟用 AJAX,而是使用 Microsoft ASP.NET AJAX 架構。在我的ASP.NET地图应用程式中,我并未撰写「原始」Javascrīpt来启用AJAX,而是使用MicrosoftASP.NETAJAX架构。 因為其中已提供一個 Visual Studio®AJAX 網站範本,用起來很簡單。因为其中已提供一个VisualStudio®AJAX网站范本,用起来很简单。只要选取这个范本,就会在Web应用程式专案中加入必要的组件参考。 為了在地图应用程式中啟用 AJAX 功能,我在原始檔中加入下列標記:为了在地图应用程式中启用AJAX功能,我在原始档中加入下列标记:
<asp:scrīptManager ID="sm" runat="server" /><asp:scrīptManagerID="sm"runat="server"/> 然后将Image控制项(非同步要求-回应执行完毕之后要更新的控制项)包装在ASP.NETAJAXUpdatePanel控制项中,如下所示:
<asp:UpdatePanel ID="up1" runat="server">    <ContentTemplate>      <asp:Image ID="Image1" runat="server" ImageUrl="~/5.JPG"                  (other attributes omitted) />    </ContentTemplate>    <Triggers>      <asp:AsyncPostbackTrigger ControlID="Button1" EventName="Click" />      <asp:AsyncPostbackTrigger ControlID="Button2" EventName="Click" />    </Triggers>  </asp:UpdatePanel><asp:UpdatePanelID="up1"runat="server">  <ContentTemplate>  <asp:ImageID="Image1"runat="server"ImageUrl="~/5.JPG"  (otherattributesomitted)/>  </ContentTemplate>  <Triggers>  <asp:AsyncPostbackTriggerControlID="Button1"EventName="Click"/>  <asp:AsyncPostbackTriggerControlID="Button2"EventName="Click"/>  </Triggers>  </asp:UpdatePanel>
就这样子而已。很棒吧!很棒吧!架構會幫我處理掉所有麻煩的細節,例如建立 XMLHTTP 物件、接聽非同步回應、錯誤處理、跨瀏覽器差異…等等。架构会帮我处理掉所有麻烦的细节,例如建立XMLHTTP物件、接听非同步回应、错误处理、跨浏览器差异…等等。

請注意,我只有在 Button1 (往北) 和 Button2 (往南) 控制項發生按一下事件時,才啟用 AJAX 非同步要求/回應,而不是在四個方向控制項上全部啟用。请注意,我只有在Button1(往北)和Button2(往南)控制项发生按一下事件时,才启用AJAX非同步要求/回应,而不是在四个方向控制项上全部启用。這是刻意的設計,目的是要說明本月專欄中的測試技術,可以同時運用在非同步和同步要求上。这是刻意的设计,目的是要说明本月专栏中的测试技术,可以同时运用在非同步和同步要求上。如果您使用本文章隨附的程式碼下載檔來執行應用程式,您會發現往北和往南的要求 (AJAX) 以及往東和往西的要求 (非 AJAX) 之間的效能,有很明顯的差別。如果您使用本文章随附的程式码下载档来执行应用程式,您会发现往北和往南的要求(AJAX)以及往东和往西的要求(非AJAX)之间的效能,有很明显的差别。

大部分傳統的測試自動化技術,通常不適用於 AJAX Web 應用程式。大部分传统的测试自动化技术,通常不适用于AJAXWeb应用程式。最常見的 Web 應用程式功能測試方法,就是以程式設計的方式,將 HTTP 要求 (對應到使用者輸入的要求) 傳送至 Web 伺服器並擷取 HTTP 回應,然後檢查回應來判斷結果是成功或失敗。最常见的Web应用程式功能测试方法,就是以程式设计的方式,将HTTP要求(对应到使用者输入的要求)传送至Web伺服器并撷取HTTP回应,然后检查回应来判断结果是成功或失败。AJAX 應用程式使用特殊的 XMLHTTP 要求,所以這種方法並不適用。AJAX应用程式使用特殊的XMLHTTP要求,所以这种方法并不适用。

另一種傳統的方法是使用 Javascrīpt 來操作 Internet Explorer DOM,將要求傳送至 Web 伺服器,接著等待 onload 事件引發 (表示回應已被用戶端收到且已載入),然後再使用 Javascrīpt 和 Internet Explorer DOM 來檢查網頁的新狀態,判斷結果是成功或失敗。另一种传统的方法是使用Javascrīpt来操作InternetExplorerDOM,将要求传送至Web伺服器,接着等待onload事件引发(表示回应已被用户端收到且已载入),然后再使用Javascrīpt和InternetExplorerDOM来检查网页的新状态,判断结果是成功或失败。這種方法的問題在於 AJAX 是以非同步方式運作,所以無法利用 onload 事件來判斷用戶端何時收到回應。这种方法的问题在于AJAX是以非同步方式运作,所以无法利用onload事件来判断用户端何时收到回应。但您可以藉由修改這個方法,來為 AJAX 應用程式建立輕量型測試自動化程式。但您可以藉由修改这个方法,来为AJAX应用程式建立轻量型测试自动化程式。您可以撰寫程式碼,在應用程式中監看應用程式狀態是否出現預期的變化,然後再將控制權移轉給回呼函數,如此就不需要依賴 onload 事件來判斷何時收到回應。您可以撰写程式码,在应用程式中监看应用程式状态是否出现预期的变化,然后再将控制权移转给回呼函数,如此就不需要依赖onload事件来判断何时收到回应。

 


測試的自動化测试的自动化

AJAX 應用程式測試控管系統是由三個檔案組成,如[圖 2]的擷取畫面所示。AJAX应用程式测试控管系统是由三个档案组成,如[图 2]的撷取画面所示。[圖 4]顯示整體控管結構的區塊圖。[图 4]显示整体控管结构的区块图。控管本身 (TestHarness.aspx) 是一個簡單的雙框架網頁。控管本身(TestHarness.aspx)是一个简单的双框架网页。右邊框架會存放測試中的 AJAX Web 應用程式。右边框架会存放测试中的AJAXWeb应用程式。左邊框架則有一個測試案例頁面,內含少數幾個 UI 和所有 Javascrīpt 程式碼,會用來操作和檢查測試中的 Web 應用程式。左边框架则有一个测试案例页面,内含少数几个UI和所有Javascrīpt程式码,会用来操作和检查测试中的Web应用程式。雖然您可以使用基本的 HTML 來製作測試控管頁面和測試案例頁面,但基於一致性,我決定在三個頁面中全部使用 .aspx 檔案。虽然您可以使用基本的HTML来制作测试控管页面和测试案例页面,但基于一致性,我决定在三个页面中全部使用.aspx档案。下列是 TestHarness.aspx 頁面的完整程式碼:下列是TestHarness.aspx页面的完整程式码:
<html>  <head>    <title>Test Harness for AJAX Web Apps</title>  </head>    <frameset cols="45%,*">      <frame src="http://localhost/AjaxTest/TestScenario001.aspx"        name="leftFrame">      <frame src="http://localhost/AjaxApplication/Default.aspx"        name="rightFrame">    </frameset>  </html><html>  <head>  <title>TestHarnessforAJAXWebApps</title>  </head>  <framesetcols="45%,*">  <framesrc="http://localhost/AjaxTest/TestScenario001.aspx"  name="leftFrame">  <framesrc="http://localhost/AjaxApplication/Default.aspx"  name="rightFrame">  </frameset>  </html>

图4 测试控管的结构(按影像可放大)

顯然,主要的測試控管頁面只是一個容器檔案。显然,主要的测试控管页面只是一个容器档案。測試案例一開始會呼叫程式定義的函數 asyncCall,此函數會讓測試中的應用程式傳送非同步 XMLHTTP 要求給 Web 伺服器,同時也會呼叫程式定義的 delay 函數。测试案例一开始会呼叫程式定义的函数asyncCall,此函数会让测试中的应用程式传送非同步XMLHTTP要求给Web伺服器,同时也会呼叫程式定义的delay函数。當 Web 伺服器在處理要求和傳回非同步回應時,delay 函數會不斷循環執行,不定時地探查 Web 應用程式的狀態。当Web伺服器在处理要求和传回非同步回应时,delay函数会不断循环执行,不定时地探查Web应用程式的状态。當用戶端收到回應且應用程式狀態也已更新之後,delay 函數就會尋找網頁狀態變更,以便進行新的非同步呼叫。当用户端收到回应且应用程式状态也已更新之后,delay函数就会寻找网页状态变更,以便进行新的非同步呼叫。

 


asyncCall 的內幕asyncCall的内幕

我的 AJAX 測試自動化技術的核心,是一對同心協力的程式定義函數:asyncCall 和 delay。我的AJAX测试自动化技术的核心,是一对同心协力的程式定义函数:asyncCall和delay。asyncCall 方法如下:asyncCall方法如下:

function asyncCall(action, checkFunc, arg, callback, pollTime)  {    numTries = 0;    action();    window.setTimeout("delay(" + checkFunc + ", " + "'" + arg +      "'" + ", " + callback + ", " + pollTime + ")", pollTime);  }functionasyncCall(action,checkFunc,arg,callback,pollTime)  {  numTries=0;  action();  window.setTimeout("delay("+checkFunc+","+"'"+arg+  "'"+","+callback+","+pollTime+")",pollTime);  }

我會在其中傳入五個引數。我会在其中传入五个引数。第一個參數 (action) 是指向某個常式的函數指標,此常式會使用 Internet Explorer DOM 來引發動作,以初始化非同步 XMLHTTP 要求。第一个参数(action)是指向某个常式的函数指标,此常式会使用InternetExplorerDOM来引发动作,以初始化非同步XMLHTTP要求。第二個參數 (checkFunc) 也是指向某個常式的函數指標,當 Web 應用程式的狀態指出非同步回應已完成時,此常式會傳回 True。第二个参数(checkFunc)也是指向某个常式的函数指标,当Web应用程式的状态指出非同步回应已完成时,此常式会传回True。第三個參數 (arg) 是傳遞給 checkFunc 函數的引數。第三个参数(arg)是传递给checkFunc函数的引数。第四個參數 (callback) 是一個函數指標,會指向非同步回應完成時要呼叫的常式。第四个参数(callback)是一个函数指标,会指向非同步回应完成时要呼叫的常式。最後一個參數 (pollTime) 會指定在搭配的 delay 函數中,各呼叫的間隔時間 (以毫秒為單位)。最后一个参数(pollTime)会指定在搭配的delay函数中,各呼叫的间隔时间(以毫秒为单位)。

我會在 asyncCall 函數內,先將全域的 numTries 計數器設定為零。我会在asyncCall函数内,先将全域的numTries计数器设定为零。我會利用這個變數來追蹤已進入 delay 函數的次數 (亦即,嘗試尋找指出非同步回應已發生的狀態幾次),以便在指定次數之後離開。我会利用这个变数来追踪已进入delay函数的次数(亦即,尝试寻找指出非同步回应已发生的状态几次),以便在指定次数之后离开。接著,我會呼叫 action 引數來引發非同步要求。接着,我会呼叫action引数来引发非同步要求。您應該知道函數名稱後面附加的括號,是呼叫函數的語法機制。您应该知道函数名称后面附加的括号,是呼叫函数的语法机制。

現在來看主要的訣竅:我會呼叫內建的 window.setTimeout 函數。现在来看主要的诀窍:我会呼叫内建的window.setTimeout函数。如您所見,setTimeout 可以接受兩個引數。如您所见,setTimeout可以接受两个引数。第一個引數是只執行一次的 Javascrīpt 陳述式。第一个引数是只执行一次的Javascrīpt陈述式。第二個引數是執行第一個引數之前的延遲時間 (以毫秒為單位)。第二个引数是执行第一个引数之前的延迟时间(以毫秒为单位)。如果您仔細看一下 asyncCall 中的程式碼,就知道 setTimeout 的第一個引數會解析成:如果您仔细看一下asyncCall中的程式码,就知道setTimeout的第一个引数会解析成:

delay(checkFunc, 'arg', callback, pollTime)delay(checkFunc,'arg',callback,pollTime)

第二個 arg 只是 pollTime。第二个arg只是pollTime。簡言之,我在延遲 pollTime 毫秒之後就叫用程式定義的 delay 函數。简言之,我在延迟pollTime毫秒之后就叫用程式定义的delay函数。

對 asyncCall 函數的呼叫如下所示:对asyncCall函数的呼叫如下所示:

asyncCall(clickNorth, imgIsTwo, "2", clickSouth, 200);asyncCall(clickNorth,imgIsTwo,"2",clickSouth,200);

此呼叫用白話文來說,就是「先叫用 clickNorth 函數,然後進入 delay 迴圈,每隔 200 毫秒就以引數 '2' 來呼叫一次 imgIsTwo 函數,最後當 imgIsTwo 傳回 True 時,就將控制權移轉給 clickSouth 函數」。此呼叫用白话文来说,就是「先叫用clickNorth函数,然后进入delay回圈,每隔200毫秒就以引数'2'来呼叫一次imgIsTwo函数,最后当imgIsTwo传回True时,就将控制权移转给clickSouth函数」。

在 asyncCall 方法的定義中,arg 必須以單引號字元括住,這一點很重要。在asyncCall方法的定义中,arg必须以单引号字元括住,这一点很重要。如果 asyncCall 函數中缺少這對單引號,就會以傳址方式來傳遞參數,如下所示:如果asyncCall函数中缺少这对单引号,就会以传址方式来传递参数,如下所示:

asyncCall(doThis, findX, "X", doThat, 200);asyncCall(doThis,findX,"X",doThat,200);

這樣的呼叫會發生錯誤,表示變數 X 未定義。这样的呼叫会发生错误,表示变数X未定义。加上單引號才會以傳值方式來傳遞參數,也才適合這種情況的需要。加上单引号才会以传值方式来传递参数,也才适合这种情况的需要。

在 asyncCall 內呼叫 window.setTimeout 函數的方式,還有另一種有趣的替代方式。在asyncCall内呼叫window.setTimeout函数的方式,还有另一种有趣的替代方式。您通常會寫成這樣:您通常会写成这样:

window.setTimeout("delay(" + checkFunc + ", " + "'" + arg +      "'" + ", " + callback + ", " + pollTime + ")", pollTime);window.setTimeout("delay("+checkFunc+","+"'"+arg+  "'"+","+callback+","+pollTime+")",pollTime);

但您可以利用 Javascrīpt 的匿名函數功能來改寫成這樣:但您可以利用Javascrīpt的匿名函数功能来改写成这样:

window.setTimeout(      function(){delay(checkFunc, arg, callback, pollTime);},       pollTime);window.setTimeout(  function(){delay(checkFunc,arg,callback,pollTime);},  pollTime);

因為已去掉引號字元和字串串連,這段程式碼比非匿名做法更簡潔。因为已去掉引号字元和字串串连,这段程式码比非匿名做法更简洁。此外,效率也會高一些,因為非匿名做法會要求瀏覽器必須建立新的指令碼環境來處理指令碼。此外,效率也会高一些,因为非匿名做法会要求浏览器必须建立新的指令码环境来处理指令码。不過,如果沒有單引號字元來隔開 arg 引數和 checkFunc,就看不出 arg 是以傳值方式來傳遞 (當然,簡單的註解就可以避免任何可能的誤解)。不过,如果没有单引号字元来隔开arg引数和checkFunc,就看不出arg是以传值方式来传递(当然,简单的注解就可以避免任何可能的误解)。

 


Delay 的內幕Delay的内幕

下列是 asyncCall 的拍擋,也就是 delay 函數:下列是asyncCall的拍挡,也就是delay函数:

function delay(checkFunc, arg, callback, pollTime)  {    ++numTries;      if (numTries > maxTries) finish();    else if (checkFunc(arg)) callback();    else window.setTimeout("delay(" + checkFunc + ", " + "'" + arg +        "'" + ", " + callback + ")", pollTime);  }functiondelay(checkFunc,arg,callback,pollTime)  {  ++numTries;    if(numTries>maxTries)finish();  elseif(checkFunc(arg))callback();  elsewindow.setTimeout("delay("+checkFunc+","+"'"+arg+  "'"+","+callback+")",pollTime);  }

delay 函數可以接受四個引數,剛好對應到傳遞給 asyncCall 函數的最後四個引數。delay函数可以接受四个引数,刚好对应到传递给asyncCall函数的最后四个引数。

在 delay 中,一開始我會累加全域 numTries 計數器。在delay中,一开始我会累加全域numTries计数器。您應該記得,我要以此來追蹤進入 delay 函數多少次,換句話說,我檢查 Web 應用程式的狀態多少次,以判斷非同步回應是否已完成。您应该记得,我要以此来追踪进入delay函数多少次,换句话说,我检查Web应用程式的状态多少次,以判断非同步回应是否已完成。如果此全域計數器大於全域 maxTries 常數,則表示 Web 應用程式在伺服器端已逾時或 XMLHTTP 回應不正確,我就會將控制權移轉給 finish 函數,來判斷實際情況。如果此全域计数器大于全域maxTries常数,则表示Web应用程式在伺服器端已逾时或XMLHTTP回应不正确,我就会将控制权移转给finish函数,来判断实际情况。

如果 delay 尚未逾時,我就呼叫 checkFunc 函數來測試指出正確回應的條件是否為 True。如果delay尚未逾时,我就呼叫checkFunc函数来测试指出正确回应的条件是否为True。如果 checkFunc 傳回 True,則我可以呼叫 callback 函數來繼續執行測試案例。如果checkFunc传回True,则我可以呼叫callback函数来继续执行测试案例。但如果 checkFunc 函數傳回 False,我就必須繼續等待,於是我再次呼叫內建的 setTimeout 函數,此函數會等待 pollTime 毫秒之後又重新呼叫 delay。但如果checkFunc函数传回False,我就必须继续等待,于是我再次呼叫内建的setTimeout函数,此函数会等待pollTime毫秒之后又重新呼叫delay。

我的 delay 函數不會直接呼叫自己,所以並非真正的遞迴函數,但因為會透過 setTimeout 函數間接呼叫自己,所以應該算是自我參考函數。我的delay函数不会直接呼叫自己,所以并非真正的递回函数,但因为会透过setTimeout函数间接呼叫自己,所以应该算是自我参考函数。結果會形成一個等待迴圈,其中會將控制權移轉給 finish 函數 (如果超過 delay 迴圈的最大次數) 或 callback 函數 (如果有 Web 應用程式狀態變成 True)。结果会形成一个等待回圈,其中会将控制权移转给finish函数(如果超过delay回圈的最大次数)或callback函数(如果有Web应用程式状态变成True)。這種方法就是所謂的事後諸葛。这种方法就是所谓的事后诸葛。當然還有其他可用方法,但這種做法已證實很簡單又有效。当然还有其他可用方法,但这种做法已证实很简单又有效。

與在 asyncCall 函數中一樣,與其以具名的函數引數來呼叫 window.setTimeout 函數,您可以改用匿名函數的功能,如下所示:与在asyncCall函数中一样,与其以具名的函数引数来呼叫window.setTimeout函数,您可以改用匿名函数的功能,如下所示:

window.setTimeout(      function(){delay(checkFunc, arg, callback);},       pollTime);window.setTimeout(  function(){delay(checkFunc,arg,callback);},  pollTime);

 


建置測試頁面建置测试页面

搞定 asyncCall 和 delay 函數之後,我現在可以開始建構我的測試案例頁面。搞定asyncCall和delay函数之后,我现在可以开始建构我的测试案例页面。[圖 5]顯示測試案例的整體結構。[图 5]显示测试案例的整体结构。

測試案例頁面的 <body> 區段只有一個標題、一個 ID 為 "comments" 的 textarea 來顯示註解,以及一個啟動自動化程式的按鈕控制項。测试案例页面的<body>区段只有一个标题、一个ID为"comments"的textarea来显示注解,以及一个启动自动化程式的按钮控制项。<head> 區段包含我的所有 Javascrīpt 程式碼。<head>区段包含我的所有Javascrīpt程式码。我宣告並初始化全域 maxTries 常數,來指定我願意進入 delay 函數的最多次數,以便透過函數判斷非同步回應是否已完成。我宣告并初始化全域maxTries常数,来指定我愿意进入delay函数的最多次数,以便透过函数判断非同步回应是否已完成。全域變數 numTries 會追蹤我已進入 delay 函數多少次。全域变数numTries会追踪我已进入delay函数多少次。全域常數 polling 則會設定每次呼叫 delay 函數的延遲時間。全域常数polling则会设定每次呼叫delay函数的延迟时间。

根據您的 AJAX Web 應用程式有多複雜而定,您可能需要修改 maxTries 和 polling 的值。根据您的AJAXWeb应用程式有多复杂而定,您可能需要修改maxTries和polling的值。在這裡,進入 10 次且各呼叫之間延遲 200 毫秒,總計也只有 2 秒,這樣的時間可能不足以讓您的 Web 伺服器處理 XMLHTTP 要求並將回應傳回到用戶端。在这里,进入10次且各呼叫之间延迟200毫秒,总计也只有2秒,这样的时间可能不足以让您的Web伺服器处理XMLHTTP要求并将回应传回到用户端。

測試案例一開始就會呼叫 runTest 函數:测试案例一开始就会呼叫runTest函数:

function runTest()  {    try    {      logRemark("Test Scenario #001");      logRemark("Starting test run\n");      step1(); // starting at 5, go N to 2    }    catch(ex) { logRemark("Fatal error: " + ex); }    }  }functionrunTest()  {  try  {  logRemark("TestScenario#001");  logRemark("Startingtestrun\n");  step1();//startingat5,goNto2  }  catch(ex){logRemark("Fatalerror:"+ex);}  }  }

我只是以程式定義的 logRemark 函數在控管主體的 <textarea> 中顯示幾則訊息,然後就將控制權移轉給 step1 函數。我只是以程式定义的logRemark函数在控管主体的<textarea>中显示几则讯息,然后就将控制权移转给step1函数。我將程式碼包裝在一個簡單的 try/catch 區塊中,以便攔截在測試回合期間擲回的任何例外狀況。我将程式码包装在一个简单的try/catch区块中,以便拦截在测试回合期间掷回的任何例外状况。我的 logRemark 函數非常簡單:我的logRemark函数非常简单:

function logRemark(comment)  {    var currComment = document.all["comments"].value;    var newComment = currComment + "\n" + comment;    document.all["comments"].value = newComment;  }functionlogRemark(comment)  {  varcurrComment=document.all["comments"].value;  varnewComment=currComment+"\n"+comment;  document.all["comments"].value=newComment;  }

我從 ID 為 "comments" 的 <textarea> 元素中擷取目前的內容,並在目前的內容中附加新行字元和新的註解文字,然後以更新的註解來取代 <textarea> 內容。我从ID为"comments"的<textarea>元素中撷取目前的内容,并在目前的内容中附加新行字元和新的注解文字,然后以更新的注解来取代<textarea>内容。這不算是很有效率的方法,但在輕量型測試自動化程式中,簡單性通常比效率考量更重要。这不算是很有效率的方法,但在轻量型测试自动化程式中,简单性通常比效率考量更重要。

step1 函數會開始操作測試中的 AJAX Web 應用程式:step1函数会开始操作测试中的AJAXWeb应用程式:

function step1()  {    logRemark("Clicking North, waiting for '2'");    asyncCall(clickNorth, checkImageSrc, "2", step2, polling);  }functionstep1()  {  logRemark("ClickingNorth,waitingfor'2'");  asyncCall(clickNorth,checkImageSrc,"2",step2,polling);  }

記錄註解之後,我便呼叫 asyncCall 函數,亦即測試自動化的核心部分。记录注解之后,我便呼叫asyncCall函数,亦即测试自动化的核心部分。這裡的呼叫可解釋為:呼叫 clickNorth 函數,然後進入 delay 迴圈,一直到 checkImgSrc("2") 傳回 True 為止,每次檢查時暫停 200 毫秒,然後將控制權移轉給 step2 函數。这里的呼叫可解释为:呼叫clickNorth函数,然后进入delay回圈,一直到checkImgSrc("2")传回True为止,每次检查时暂停200毫秒,然后将控制权移转给step2函数。如果呼叫 delay 迴圈超過 10 次 (maxTries),控制權將移轉給 finish 函數。如果呼叫delay回圈超过10次(maxTries),控制权将移转给finish函数。

clickNorth 函數非常簡單:clickNorth函数非常简单:

function clickNorth()  {    var btnNorth = parent.rightFrame.document.all["Button1"];    if (!btnNorth) throw "Did not find btnNorth";    btnNorth.click();  }functionclickNorth()  {  varbtnNorth=parent.rightFrame.document.all["Button1"];  if(!btnNorth)throw"DidnotfindbtnNorth";  btnNorth.click();  }

我使用 Internet Explorer DOM 來取得 Button1 控制項的參考,然後叫用其 click 方法。我使用InternetExplorerDOM来取得Button1控制项的参考,然后叫用其click方法。請注意,因為我的自動化程式會放在框架中,而 AJAX 應用程式則是放在另一個框架中,所以為了要從我的測試指令碼中存取應用程式上的控制項,我必須使用 parent 關鍵字來「上移」一層,才能使用應用程式容器的框架 ID。请注意,因为我的自动化程式会放在框架中,而AJAX应用程式则是放在另一个框架中,所以为了要从我的测试指令码中存取应用程式上的控制项,我必须使用parent关键字来「上移」一层,才能使用应用程式容器的框架ID。我喜歡使用 document.all 集合來取得網頁控制項的參考,但我有些同事則喜歡使用 getElementById 方法,如下所示:我喜欢使用document.all集合来取得网页控制项的参考,但我有些同事则喜欢使用getElementById方法,如下所示:

var btnNorth = parent.rightFrame.document.getElementById("Button1");varbtnNorth=parent.rightFrame.document.getElementById("Button1");

checkImageSrc 函數會讓自動化程式知道測試中之 AJAX 應用程式上的 Image1 控制項,何時由非同步回應完成更新:checkImageSrc函数会让自动化程式知道测试中之AJAX应用程式上的Image1控制项,何时由非同步回应完成更新:

function checkImageSrc(target)  {    try    {      var s = parent.rightFrame.document.all["Image1"].src;      return s.indexOf(target) >= 0;    }      // traps case where Image1 is not yet loaded      // logRemark("Error in checkImageSrc(): " + ex);    catch(ex) { return false; }  }functioncheckImageSrc(target)  {  try  {  vars=parent.rightFrame.document.all["Image1"].src;  returns.indexOf(target)>=0;  }  //trapscasewhereImage1isnotyetloaded  //logRemark("ErrorincheckImageSrc():"+ex);  catch(ex){returnfalse;}  }

基本上,我只是檢查 Image1 控制項的 src 屬性是否包含某個目標字串。基本上,我只是检查Image1控制项的src属性是否包含某个目标字串。例如,如果影像的 src 屬性包含 "2",像是地圖 2 所用的檔案名稱 "~/2.jpg",則呼叫 checkImageSrc("2") 會傳回 True。例如,如果影象的src属性包含"2",像是地图2所用的档案名称"~/2.jpg",则呼叫checkImageSrc("2")会传回True。您在撰寫自己的 AJAX 自動化程式時必須建立適當的檢查函數。您在撰写自己的AJAX自动化程式时必须建立适当的检查函数。例如,假設您的 Web 應用程式會更新 ID 為 "TextBox1" 的 TextBox 控制項。例如,假设您的Web应用程式会更新ID为"TextBox1"的TextBox控制项。則一個可能的檢查函數如下:则一个可能的检查函数如下:

function checkTextBoxValue(target)  {    try    {      var s = parent.rightFrame.document.all["TextBox1"].value;      return s.indexOf(target) >= 0;    }      // traps case where TextBox1 is not yet loaded      // logRemark("Error in checkTextBoxValue(): " + ex);    catch(ex) { return false; }  }functioncheckTextBoxValue(target)  {  try  {  vars=parent.rightFrame.document.all["TextBox1"].value;  returns.indexOf(target)>=0;  }  //trapscasewhereTextBox1isnotyetloaded  //logRemark("ErrorincheckTextBoxValue():"+ex);  catch(ex){returnfalse;}  }

請注意我在 checkImageSrc 函數中的 try/catch 區塊。请注意我在checkImageSrc函数中的try/catch区块。不同於大多數 try/catch 都是用來攔截非預期錯誤狀況的情形,我在這裡是將 try/catch 納入為正常函數邏輯的一部分。不同于大多数try/catch都是用来拦截非预期错误状况的情形,我在这里是将try/catch纳入为正常函数逻辑的一部分。理由是在重新繪製網頁期間,將無法取得 Image1 控制項的參考,因此,只要一嘗試取得就會擲回例外狀況。理由是在重新绘制网页期间,将无法取得Image1控制项的参考,因此,只要一尝试取得就会掷回例外状况。這並非表示 Web 應用程式的狀態有錯誤,只是表示應用程式的狀態不完整,所以我才可以攔截例外狀況並傳回 False。这并非表示Web应用程式的状态有错误,只是表示应用程式的状态不完整,所以我才可以拦截例外状况并传回False。只要取消 logRemark 的註解並設定很短的輪詢時間 (例如 10 毫秒),就可以看到這個現象。只要取消logRemark的注解并设定很短的轮询时间(例如10毫秒),就可以看到这个现象。

順便一提,在正常函數邏輯中使用 try/catch 通常不是很好的程式撰寫風格,但在這個案例中,我認為其中的簡單性值得讓 try/catch 成為正常邏輯流程的一部分。顺便一提,在正常函数逻辑中使用try/catch通常不是很好的程式撰写风格,但在这个案例中,我认为其中的简单性值得让try/catch成为正常逻辑流程的一部分。

我在測試案例指令碼中定義的 step2、step3、step4 及 step5 函數,全部都很類似 step1 函數。我在测试案例指令码中定义的step2、step3、step4及step5函数,全部都很类似step1函数。step2 函數會呼叫 clickEast 函數,等待地圖顯示區域出現 "3",然後呼叫 step3。step2函数会呼叫clickEast函数,等待地图显示区域出现"3",然后呼叫step3。step3 函數則會呼叫 clickSouth 函數,並等待 "6" 出現,依此類推。step3函数则会呼叫clickSouth函数,并等待"6"出现,依此类推。step5 函數會呼叫簡短的中繼 finish 函數 step6:step5函数会呼叫简短的中继finish函数step6:

function step6()  {    finish();  }functionstep6()  {  finish();  }

我在這裡只是將控制權移轉給真正的 finish 函數 (請參閱[圖 6])。我在这里只是将控制权移转给真正的finish函数(请参阅[图 6])。您應該記得,finish 函數也會負責處理超過 delay 函數之最大呼叫次數的狀況。您应该记得,finish函数也会负责处理超过delay函数之最大呼叫次数的状况。

首先,我會處理全域計數器 numTries 超過全域常數 maxTries 的狀況。首先,我会处理全域计数器numTries超过全域常数maxTries的状况。這種狀況有兩種原因:可能是我的 AJAX 應用程式的邏輯正確,只是應用程式花太多時間來更新頁面狀態,或是應用程式的邏輯錯誤,以致於檢查函數找不到正確的狀態。这种状况有两种原因:可能是我的AJAX应用程式的逻辑正确,只是应用程式花太多时间来更新页面状态,或是应用程式的逻辑错误,以致于检查函数找不到正确的状态。我在這裡只是任意選擇一個簡單的方法,來解決失敗的測試案例。我在这里只是任意选择一个简单的方法,来解决失败的测试案例。

finish 中的第二個邏輯分支會檢查應用程式的最終狀態,以判斷結果是成功或失敗。finish中的第二个逻辑分支会检查应用程式的最终状态,以判断结果是成功或失败。在這個案例中,我會檢查最後的地圖影像是否在 "8",因為假設我從 "5" 開始,然後連續按下 [往北]、[往東]、[往南]、[往南]、[往西] 控制項,最後的地圖影像就應該在 "8"。在这个案例中,我会检查最后的地图影像是否在"8",因为假设我从"5"开始,然后连续按下[往北]、[往东]、[往南]、[往南]、[往西]控制项,最后的地图影像就应该在"8"。

Back to top


延伸測試控管延伸测试控管

我在這裡說明的測試自動化系統屬於非常輕量型的系統,是特別為了讓您自行修改而設計的。我在这里说明的测试自动化系统属于非常轻量型的系统,是特别为了让您自行修改而设计的。有如往常,由於文章要盡量精簡化,所以我刪掉了大部分的錯誤檢查程式碼,僅保留彰顯重點的部分。有如往常,由于文章要尽量精简化,所以我删掉了大部分的错误检查程式码,仅保留彰显重点的部分。不過,您應該要加入相關的錯誤捕捉機制 - 因為測試的自動化本來就會有很多意外狀況,所以錯誤的處理只能算是常態,不是例外。不过,您应该要加入相关的错误捕捉机制-因为测试的自动化本来就会有很多意外状况,所以错误的处理只能算是常态,不是例外。

請注意,測試控管並未完全自動化:您必須手動按一下 [執行測試 (Run Test)] 按鈕來啟動自動化程式。请注意,测试控管并未完全自动化:您必须手动按一下[执行测试(RunTest)]按钮来启动自动化程式。有幾種方式可以完全將控管自動化。有几种方式可以完全将控管自动化。一種簡單的做法是在測試控管框架中加入 onload 處理常式:一种简单的做法是在测试控管框架中加入onload处理常式:

<frame src="http://localhost/AjaxApplication/Default.aspx"    name="rightFrame"    ōnload="leftFrame.launch();" ><framesrc="http://localhost/AjaxApplication/Default.aspx"  name="rightFrame"  ōnload="leftFrame.launch();">

在此情況下,測試案例程式碼中的 launch 函數定義如下:在此情况下,测试案例程式码中的launch函数定义如下:

var started = false;  function launch()  {    if (!started) runTest();  }varstarted=false;  functionlaunch()  {  if(!started)runTest();  }

此外,本專欄中描述的 runTest 函數需要進行修改,以下列陳述式開頭:此外,本专栏中描述的runTest函数需要进行修改,以下列陈述式开头:

started = true;started=true;

載入含有測試中 AJAX Web 應用程式的右邊框架時,就會呼叫 lanuch 函數。载入含有测试中AJAXWeb应用程式的右边框架时,就会呼叫lanuch函数。launch 第一次執行時,全域變數 started 為 False,且控制權會移轉給 runTest 函數,此函數會將變數 started 設定為 True,然後執行自動化程式。launch第一次执行时,全域变数started为False,且控制权会移转给runTest函数,此函数会将变数started设定为True,然后执行自动化程式。Web 應用程式在後來載入頁面時 (由一般的 HTTP 要求/回應動作引發),將會呼叫 launch 函數,但因為全域的 started 為 True,所以控制權不會再移轉給 runTest。Web应用程式在后来载入页面时(由一般的HTTP要求/回应动作引发),将会呼叫launch函数,但因为全域的started为True,所以控制权不会再移转给runTest。有了完全自動化的測試控管之後,您可以建立多個控管頁面和案例頁面,然後藉由將如下的陳述式放入 BAT 檔案中,再以 Windows 工作排程器來啟動自動化程式,來執行多個案例:有了完全自动化的测试控管之后,您可以建立多个控管页面和案例页面,然后藉由将如下的陈述式放入BAT档案中,再以Windows工作排程器来启动自动化程式,来执行多个案例:

iexplore http://localhost/AjaxTest/TestHarness01.aspx  iexplore http://localhost/AjaxTest/TestHarness02.aspxiexplorehttp://localhost/AjaxTest/TestHarness01.aspx  iexplorehttp://localhost/AjaxTest/TestHarness02.aspx

請注意,我的輕量型系統的一項弱點,是您無法直接將系統參數化。请注意,我的轻量型系统的一项弱点,是您无法直接将系统参数化。雖然您可以很容易地將測試案例頁面的通用 Javascrīpt 程式碼放在一個獨立的 .js 檔案中,但若要從單一測試控管頁面來執行多個測試案例,就不是那麼簡單。虽然您可以很容易地将测试案例页面的通用Javascrīpt程式码放在一个独立的.js档案中,但若要从单一测试控管页面来执行多个测试案例,就不是那么简单。這並非做不到,我只是說這不是一件簡單的工作。这并非做不到,我只是说这不是一件简单的工作。

AJAX 測試自動化的另一項可延伸作業,是將儲存測試案例結果的程序自動化。AJAX测试自动化的另一项可延伸作业,是将储存测试案例结果的程序自动化。我提出的系統不會記錄結果,但您可以利用各種方式來記錄。我提出的系统不会记录结果,但您可以利用各种方式来记录。其中一種做法就是將 Form 元素放入測試案例頁面中,再將含有測試案例結果的文字欄位放在 form 中,然後將結果 POST (傳遞) 到伺服器,如下所示:其中一种做法就是将Form元素放入测试案例页面中,再将含有测试案例结果的文字栏位放在form中,然后将结果POST(传递)到伺服器,如下所示:

<form name="resultForm" method="Post" action="saveResults.aspx">    <p>Result: <input type="text" name="result"></p>    <p><input type="submit" name="saver" value="Save Results"></p>  </form><formname="resultForm"method="Post"action="saveResults.aspx">  <p>Result:<inputtype="text"name="result"></p>  <p><inputtype="submit"name="saver"value="SaveResults"></p>  </form>

您可以利用類似的程式碼,提供 pass/fail 值給 finish 函數內的 result 欄位:您可以利用类似的程式码,提供pass/fail值给finish函数内的result栏位:

if (is.indexOf("8") >= 0)  {    logRemark("\n*Pass*");    theForm.result.value = "Pass";  }  else  {    logRemark("\n*FAIL*");    theForm.result.value = "Fail";  }if(is.indexOf("8")>=0)  {  logRemark("\n*Pass*");  theForm.result.value="Pass";  }  else  {  logRemark("\n*FAIL*");  theForm.result.value="Fail";  }

如此一來,您就可以按一下 [儲存結果 (Save Results)] 按鈕來手動儲存案例結果,或在案例程式碼中加入下列程式碼來自動化結果的儲存:如此一来,您就可以按一下[储存结果(SaveResults)]按钮来手动储存案例结果,或在案例程式码中加入下列程式码来自动化结果的储存:

document.all["theForm"].submit();document.all["theForm"].submit();

 


結論

TAG: Web测试

nic ever 引用 删除 nic162534   /   2007-06-05 14:45:03
你好,我看你还到过我的blog,所以就顺着脚印过来了,觉得你挺厉害的!我是个新手,我想问问现在比较好用的功能测试工具是哪个,而且比较用的多的!好迷茫!不知道该学习哪个,winrunner,loadrunner,rational robot,——,这么多哪个好用阿!!!
 

评分:0

我来说两句

风在吹

风在吹

51Testing网站负责人,负责51testing全站.

日历

« 2024-04-18  
 123456
78910111213
14151617181920
21222324252627
282930    

数据统计

  • 访问量: 55706
  • 日志数: 34
  • 图片数: 2
  • 文件数: 2
  • 书签数: 16777215
  • 建立时间: 2006-11-21
  • 更新时间: 2016-12-16

RSS订阅