Web 應用程序日漸復雜,對它們的測試工作也變得越來越重要。有很多測試技術可供你選擇使用。例如,在 2005 年 4 月份的 MSDN 雜志中,我描述了一個基于 JScript 的簡單系統,它使用 IE 的文檔對象模型(Internet Explorer Document Model)來完整地測試某個 Web 應用程序的用戶界面。這個技術很有效,但是在幾個方面存在著缺陷。我的一些同事問我是否能使用 .Net 框架編寫功能更強,但仍然是輕量級的 Web 應用程序用戶界面自動化測試程序。在這個月的專欄文章中,我將向你們展示怎樣達到這個目的。這個低層技術要求直接調用 fmshtml.dll 和 shdocvw.dll 動態鏈接庫來訪問和操縱 IE 瀏覽器客戶區中的 HTML 對象。
讓我們從一個屏幕截圖開始,如圖1所示。圖1表明我正在測試一個虛構的 Web 應用程序,它搜索一個雇員信息數據倉庫。用戶能通過雇員的姓和名進行搜索。應用程序顯示雇員的姓、名,如果大小寫都匹配,那么顯示相應的生日。通過手工測試這個 Web 應用程序的用戶界面是非常沉悶的、低效的,并且可能錯誤百出。更好的一種方法是通過測試自動化。該自動化啟動一個IE實例,裝載正在進行的測試,操縱應用程序,并檢查應用程序的正確性狀態。
圖1 測試執行例子
當然,一個實際的 Web 應用程序肯定比這更加復雜,但是我向你們展示的技術能夠用來測試基于 IE 的 Web 應用程序。在接下來的一節中,我將簡要介紹正進行測試的 Web 應用程序,這樣你就能理解我在測什么和怎么測。我也會詳細解釋產生圖1的測試場景代碼,也會描述怎樣改編和擴展這里描述的技術。
被測試的 Web 應用程序
我的 Web 應用程序,WebForm1.aspx,是一個ASP.NET程序,但是本專欄描述的技術能夠應用于任何類型的 Web 應用程序中。我的程序包括:兩個單選按鈕控件來告訴應用程序的控制邏輯要搜索的是哪一種字段;一個文字輸入控件來接受用戶的搜索項;一個按鈕控件來啟動搜索;以及一個文本區,用來顯示結果。在文本區的下方,有一個標志欄,它顯示“搜索完成”,它的初始狀態是隱藏。我這里闡述的技術的兩個優點是:我不用對該Web應用程序進行插裝,也不需要接觸應用程序的源代碼。但是,我需要知道Web應用程序中各種HTML元素的ID號,通過View/Source,我能輕易做到這一點(不過要求ID必須是靜態的,不能是動態的)。例如,按鈕控件有ID號“Button1”,而“Last Name單選按鈕”控件有ID號“RadioButtonList1_0”。當然,如果你能夠接觸程序的源代碼,你肯定已經有了這些控件的ID信息。
我使用Visual Studio.NET來創建這個用來測試的虛構的Web應用程序。從Visual Studio.NET的設計角度來看,我添加了三個標簽控件、一個單選按鈕、一個文本區、一個按鈕控件和一個列表框控件。為簡化起見,我使用了控件的默認名稱“Label1”、“TetBox1”等。相關代碼列在圖2中。我聲明了一個Employee類和一個ArrayList對象來存放Employee對象。在方法Page_Load中,我向ArrayList中增加了虛構的雇員數據。在實際的應用程序中,你的數據可能來自于SQL Server數據庫或XML文件。但就用戶界面測試自動化來說,數據來自哪里是無關緊要的。
方法Button1_Click中清空列表框控件,從文本框控件中獲取過濾子串,檢查單選按鈕以便確定是搜索姓還是搜索名,在內存數據中搜索相匹配的,并把匹配的雇員信息顯示出來。我得強調一下,為了使應用程序例子簡單,我在這里可能用了一些不好的編碼技術。這與你可能遇到的某種情況非常類似——你所處理的應用程序是發布前的,代碼可能并沒有經過優化。我的Web應用程序顯然是虛構的,但是通過用戶界面來測試任何Web應用程序的基本要素是應用程序隨著每一個HTTP請求-應答對的狀態轉換。換句話說,就算你打算測的Web應用程序要求訪問SQL Server數據庫或者進行了非常復雜的處理,它只是一種狀態轉換,并且這種轉換會在HTTP應答和用戶界面中顯示出來。
測試自動化
測試場景系統由一個單一文件組成。我打算把我的測試集實現為一個C??刂婆_應用程序,但你將會看到,我能使用任何與.Net兼容的語言?。ɡ?,Visual Basic .NET),該技術也能用于任何程序(例如,一個Windows程序)和測試框架(例如,NUnit)。場景的整體結構顯示在圖3中。首先,我向“Microsoft Internet Controls”這個優秀的COM組件添加了一個引用(reference)。這是shdocvw.dll模塊的一個別名,該模塊擁有操作基于Windows的瀏覽器(例如IE和Windows Explorer)的能力。然后我向Microsoft.mshtml.Net組件添加了一個引用。這是mshtml.dll模塊的一個別名,該模塊擁有訪問HTML元素的能力。我向兩個相應的名字空間增加了“using”聲明,這樣我就不需完整驗證他們的類了。我也針對System.Diagnostics和System.Threading分別增加了“using”聲明,這樣我容易引用前者的Process類,也能在我合適的時候引用后者的Thread.Slepp方法來暫停我的自動化過程。
這項技術的一個關鍵是要有能力確定一個Web頁面/文檔/應用程序何時已經在IE中充分裝載了。我定義了一個類范圍的AutoResetEvent對象documentComplet,我使用該對象來標記一個已經充分裝載了文檔的等待線程。
static AutoResetEvent documentComplete = new AutoResetEvent(false);
馬上,我就會詳細介紹這里的細節。我由向命令行中打印一條狀態信息來開始我的測試場景。然后我聲明了一個Boolean類型的變量pass并把它設為false。我假設測試場景會失敗,如果我檢測到應用程序的最后狀態是正確的,我修正我的假設并把pass變量設為true。下一步我聲明了一個InternetExplore對象“ie”:
Console.WriteLine("\nStart test run");bool pass = false;InternetExplorer ie = null;
InternetExplorer類是在SHDocVw名字空間中定義的。該類有很多方法操縱Internet Explorer的一個實例,但是由你決定啟動Internet Explorer并把兩者相關聯,如下所示:
// launch explorerConsole.WriteLine("\nLaunching an instance of IE");Process p = Process.Start("iexplore.exe", "about:blank");if (p == null) throw new Exception("Could not launch IE");Console.WriteLine("Process handle = " + p.MainWindowHandle.ToString());// find all active browsersSHDocVw.ShellWindows allBrowsers = new SHDocVw.ShellWindows();Console.WriteLine("Number active browsers = " + allBrowsers.Count);if (allBrowsers.Count == 0) throw new Exception("Cannot find IE");
我使用System.Diagnostics.Process名字空間中的Start方法來啟動一個Internet Explorer(iexplore.exe)并裝載空白頁面“about:blank”;Start方法返回對創建進程對象的一個引用。然后我初始化了一個名為allBrowsers的ShellWindows對象。這個對象掌握了對所有ShellWindows對象的引用,也掌握了對瀏覽器的引用(包括Windows Explorer的實例,我剛才啟動的Internet Explorer的實例和之前啟動的Internet Explorer的實例)。我使用Count屬性來顯示關于目前活動瀏覽器的診斷信息,以便確保Internet Explorer成功啟動了。測試自動化的下一步是把新的進程與Internet Explorer對象關聯起來:
Console.WriteLine("Attaching to IE");for(int i=0; i < allBrowsers.Count && ie == null; i++){ InternetExplorer e = (InternetExplorer)allBrowsers.Item(i); if (e.HWND == (int)p.MainWindowHandle) ie = e;}if (ie == null) throw new Exception("Failed to attach to IE");
可能有好幾個Internet Explorer的實例正在運行,所以我需要辨明哪個是我的測試場景啟動的,以便我能把我的InternetExplorer變量ie與正確的實例關聯起來。記住,我把測試啟動的Internet Explorer捕獲到一個進程對象p中了。所以我遍歷ShellWindows中的每一個對象,檢查他們的句柄或指針是否和測試啟動的進程的主窗口句柄一致。我有時候采用的一個替換的方法是假設只有我的測試Internet Explorer實例允許運行。如果有多個Internet Explorer實例運行,我拋出一個異常。這個假設運行我把測試Internet Explorer與下面的代碼簡單關聯起來:
ie = (InternetExplorer)allBrowsers.Item(0);
你實際采用何種方法取決于你的實際測試場景。既然我建立了我的測試InternetExplorer對象,我能注冊我之前提到的DocumentComplete事件句柄:
ie.DocumentComplete += new DWebBrowserEvents2_DocumentCompleteEventHandler(ie_DocumentComplete);
簡單來說,當InternetExplorer的DocumentComplete事件發生時,調用用戶定義的ie.DocumentComplete方法。如果你回頭去看圖中的代碼,你能看到我如此定義了該方法:
private static void ie_DocumentComplete(object pDisp, ref object URL){ documentComplete.Set();}
ie_DocumentComplete 方法調用了我早些時候在測試類中定義的AutoResetEvent對象中的Set方法。簡而言之,現在我能暫停我的執行線程,直到我的InternetExplorer對象充分裝載了。我會立即向你展示怎樣具體做這件事情?,F在我瀏覽正在進行測試的Web應用程序,等到應用程序充分裝載:
Console.WriteLine("\nNavigating to the Web app");object missing = Type.Missing;ie.Navigate("http://localhost/LowLevelWebUIAutomationApp/WebForm1.aspx", ref missing, ref missing, ref missing, ref missing);documentComplete.WaitOne();
我使用InternetExplorer.Navigate方法來裝載我的Web應用程序。Navigate接受數個可選參數,但是在這個例子中,我不需要任何參數。注意,我調用了documentComplete對象的WaitOne方法。WaitOne將停止我的執行線程,直到應用程序充分裝載到了Internet Explorer中。在這個例子中,我沒有提供一個超時值,所以我會不停的等待,但你很可能會向WaitOne傳遞一個代表超時毫秒數的整型值。下一步我設定Internet Explorer為一個固定的大小,并獲得Web應用程序文檔的一個引用。
Console.WriteLine("Setting IE to 525x420");ie.Width = 525;ie.Height = 420;HTMLDocument theDoc = (HTMLDocument)ie.Document;