Robot Framework + Selenium2Library環境下,結合Selenium Grid實施分佈式自動化測試

         最近一段時間,公司在推行自動化測試流程,本人有幸參與了自定義通用控件的關鍵字封裝和腳本輔助編寫、數據驅動管理、測試用例執行管理等一系列工具軟件的研發工做,積累了一些經驗,在此與你們作一下分享,也算是作一個總結吧,但願能給你們帶來啓發和幫助。因爲業界沒有成熟的解決方案可供參考,本人在研究過程當中也是摸着石頭過河,紕漏之處在所不免,若是你們有更好的方案,敬請不吝賜教。java

 

分佈式並行執行用例需求背景python

        公司的產品屬於web app,採用的是Robot Framework + Selenium2Library 做爲自動化測試的框架。腳本開發完畢,在推廣試用的過程當中,測試人員反饋了一個問題:當case數量不少的時候,須要執行很長的時間才能跑完,這每每沒法跟上產品發佈迭代的節奏。他們的要求是:100個case要求在一個小時以內跑完(平均一個case須要3到5分鐘)。通過對比和研究,最終決定利用Selenium Grid來將這些case分散到多臺機器去執行,經過分佈式並行執行的方式,壓縮執行時間。web

        關於Selenium Grid的介紹、運行原理、安裝部署等細節,請參考 http://blog.csdn.net/five3/article/details/9428655,在此不做詳述。算法

 

分佈式並行執行用例管理工具一覽
數據庫

        爲了更好的貼合公司實際需求,我開發了一個winform應用程序來管理case的執行過程。主要功能包括:安全

        1. 支持本機執行和分佈式並行執行這兩種執行方式session

        2. 因爲環境和框架自己的不穩定性,須要在腳本出錯的狀況下(包括case fail掉了或者是robot framework框架出錯),能自動對出錯的case作必定的重試,以此消除環境的不穩定因素對執行過程的影響。多線程

        3. 支持有條件的並行(有條件並行的概念將在後面介紹)。app

        工具界面以下(左側是case選擇界面,右側是選項面板和執行結果信息):框架

       

       

Robot Framework 與 Selenium Grid 之間的橋樑

        Selenium Grid安裝好以後,是一個獨立的系統,與外界沒有任何聯繫,它不會自動獲取case、自動分發請求。因此,要利用Selenium Grid,首先要研究如何把case執行請求發送給Grid。

        網上有很多介紹Selenium Grid的文章,多以webdriver的API做爲與Selenium Grid交互的途徑,典型代碼以下:

DesiredCapabilities dc = DesiredCapabilities.firefox();
WebDriver dr = new RemoteWebDriver(new URL("http://192.168.40.67:5555/wd/hub"),dc);
dr.get("http://www.baidu.com");

        可是Robot Framework如何與Selenium Grid進行交互呢?咱們知道,Robot Framework只是一個自動化測試框架而已,它之因此可以測試各類類型的應用程序,是由於它能夠掛接不一樣的庫,好比,掛接Selenium2Library,就能夠測試web app,最終執行測試的動做仍是由相應的庫來完成的。

        Selenium2Library對selenium類庫作了一下包裝,造成了相應的python類,與selenium相關的關鍵字就是調用這些類的方法,好比:關鍵字 「open browser」 就是調用的_BrowserManagementKeywords這個類的open_browser方法(python文件路徑爲C:\Python27\Lib\site-packages\Selenium2Library\keywords\_browsermanagement.py,安裝方式不一樣,路徑會略有不一樣)。

        研究這個方法的代碼能夠知道,參數remote_url的做用與上面RemoteWebDriver類構造方法的第一個參數的做用是相似的。因而,咱們能夠考慮在這個參數上作文章。那麼,咱們是否須要修改全部case的腳本,以下圖所示加上這個參數呢?

        顯然,這種方式很差。兩個緣由:1. 須要大量的修改現有的腳本;2. 參數是寫死的,沒法與工具選項設置關聯起來。

        因此,在不修改腳本的狀況下,要同時支持在本地和分佈式執行用例,最直接的方法就是修改_BrowserManagementKeywords類的open_browser方法,由於無論是哪一種執行方式,這個方法都會被調用。

        另外,工具選項面板上的執行方式選項如何通知給這個方法呢?個人辦法是經過一個全局橋樑文件來做爲彼此通訊的紐帶,工具依據選項設置生成或者刪除橋樑文件;open_browser方法判斷文件是否存在並讀取文件內容做爲remote_url參數的值。

        工具選項設置代碼片斷:

//Hub URL存放文件
private const string SeleniumGridHubUrlFilePath = "c:\\SeleniumGridHubUrl.txt";
//生成、刪除remote url參數文件            
if (this.radioButton1.Checked)
{
    File.Delete(SeleniumGridHubUrlFilePath);
}
else
{
    File.WriteAllText(SeleniumGridHubUrlFilePath,string.Format("http://{0}:4444/wd/hub",this.textBox3.Text.Trim()));
}

        py文件方法修改:

 

Robot Framework 命令行介紹

        打通了Robot Framework與Selenium Grid之間的聯繫以後,咱們開始研究如何經過工具執行case。

        在腳本開發調試過程當中,通常用的RIDE集成環境,可是若是用工具來管理執行過程的話,就沒法再使用RIDE了,只能想別的辦法。若是仔細觀察過RIDE的一些窗口輸出信息的話,必定能注意到它會把本次執行對應的DOS command打印出來,以下圖所示:

         其實,RIDE就是調用這個命令行來執行用例的,因此咱們也能夠經過調用命令行的方式來執行用例。因爲工具會涉及到執行用例、錯誤用例重試、合併報告這三種功能,因此這裏就簡單介紹一下這三個命令。更多命令,能夠經過在dos命令行裏面輸入 pybot --help 查詢。

       

        1. 執行用例

        --test : 指定要執行的case名稱(全路徑)       --outputdir : log存放路徑    --argumentfile : 選項文件路徑      參數1 : 要執行的suite路徑

        示例:

 

        2. 重試用例

        -R : 表示要重試錯誤用例    -d : 本次執行log存放路徑    參數一 : 須要執行重試的output.xml文件路徑    參數二 : suite路徑

        示例:

pybot -R D:\logs\2014-09-05-18-40-42\remote_1\tag__batch_0\output.xml -d D:\logs\2014-09-05-18-40-42\remote_2\tag__batch_0 D:\自動化測試111\成本系統

 

        3. 合併報告(使用rebot指令,詳細說明能夠經過 rebot  --help 查看)

        -N :  重命名報告    -d :產生的新log的存放路徑      參數一 : 須要合併的log路徑(支持通配符)

        示例:

rebot -N 第1次執行 -d D:\logs\2014-09-05-18-40-42\1 D:\logs\2014-09-05-18-40-42\remote_1\*\output.xml

 

        工具就是經過調用上述三個命令並輔以必定的流程控制邏輯來完成執行、重試、合併報告的功能。

        代碼片斷:首先生成一個批處理文件,而後啓動一個DOS命令行窗口執行這個bat文件

//生成參數文件
string argFilePath = @"c:\RFSCommandLine\" + batchTime + "_argfile.txt";
string argFileContent = "--outputdir\r\n" + Path.Combine(logFolder, "1") + "\r\n" + BuildTestCaseCommandOption(selectedCases);
File.WriteAllText(argFilePath, argFileContent, Encoding.GetEncoding("utf-8"));

//生成批處理文件
batchFilePath = @"c:\RFSCommandLine\" + batchTime + "_1.bat";
content = string.Format(@"pybot.bat --argumentfile {0} {1}", argFilePath, currentProjectPath);
File.WriteAllText(batchFilePath, content, Encoding.GetEncoding("gb2312"));
//調用批處理文件
try
{
    Process p = new Process();
    p.StartInfo.FileName = batchFilePath;
    p.Start();
    p.WaitForExit();
}

 

工具設計思路分析

        一. 基本概念介紹

        1. hub分發請求的粒度

        剛開始研究Selenium Grid的時候,覺得只要把全部選中的case放到一個批處理文件裏面,而後運行該批處理,這樣就能夠把執行請求發送給hub,hub就會自動分發請求到各個空閒節點。但事實證實,不是這樣的,hub只會隨機選擇一個空閒節點,而後一根筋的把全部的case都分發到該節點執行,而不是分發給全部空閒的節點。這說明hub分發請求的粒度是批處理DOS命令行爲單位的,而不是單個的case,一個DOS命令行裏的全部case始終是在同一個節點上執行,不會被打散。爲了能多節點並行執行,咱們須要人爲的將這些case拆成一批一批的,也就是下面要介紹的請求批次的概念。

        2. 請求批次

        所謂「請求批次」,指的是按照某種算法邏輯,將須要執行的case列表進行拆分,造成批次。舉個例子,我有100條case,若是設定每一個批次包含10個case,那麼咱們能夠將這100個case拆分紅10個批次。若是我同時將這10個批次發送給hub,那麼hub此時就會尋找全部空閒的節點,並將這些批次調度分發給這些節點執行,這樣就能夠達到並行的目的了。我這裏是採用多線程技術完成這個任務,一個線程啓動一個批處理文件,一個批處理文件中包含着一個批次的case,三者之間是一一對應的關係。示意圖以下:

 

代碼片斷:

for (int i = 0; i < splittedCaseList.Count; i++)
{
    string argFilePath = @"c:\RFSCommandLine\" + batchTime + "_argfile_tag_" + tag + "_" + i.ToString() + ".txt";
    string argFileContent = "--outputdir\r\n" + Path.Combine(Path.Combine(logFolder, "remote_1"), "tag_" + tag + "_batch_" + i.ToString()) + "\r\n" + BuildTestCaseCommandOption(splittedCaseList[i]);
    File.WriteAllText(argFilePath, argFileContent, Encoding.GetEncoding("utf-8"));

    batchFilePath = @"c:\RFSCommandLine\" + batchTime + "_1_tag_" + tag + "_" + i.ToString() + ".bat";
    content = string.Format(@"pybot.bat --argumentfile {0} {1}", argFilePath, currentProjectPath);

    File.WriteAllText(batchFilePath, content, Encoding.GetEncoding("gb2312"));

    Thread thread = new Thread(new ParameterizedThreadStart(RunRemoteCommand));
    threads.Add(thread);
    thread.Start(batchFilePath);
}

 

        3. 串行與並行

        「並行」指的是多個批次的case能夠分散到不一樣的節點同時執行;「串行」指的是有些流程必須按順序執行,好比:只有完成了第一次執行的全部批次以後,才能夠進入第一次重試環節,第一次重試結束了,才能夠開始第二次重試,全部重試結束以後,才能夠開始合併報告。那麼啓動了那麼多的線程,咱們如何管理好它們的執行順序呢?這裏能夠利用線程同步機制,在串行流程中,適當地作一下等待。代碼片斷:

//阻塞調用線程,等待全部並行線程都結束,纔開始後續串行操做
foreach
(Thread thread in threads) { thread.Join(); }

 

        4. 全局資源與並行干擾(有條件並行)

        "全局資源「指的是各個模塊功能共用的一些配置、選項、或者數據庫中公共的表之類的一些全局型的資源。全局資源具備排他性,好比:不能一個case把某個選項開關打開,而另一個case同時又把這個開關關掉。若是這樣的話,這兩個case之間互相形成干擾,會致使case執行出錯,或者測試結果不許確。因此說,並行並非絕對的並行,而是有條件的,咱們須要識別出這種隱藏在業務邏輯中的排斥關係。

        5. tag分組

         目前,我是採用按tag分組的辦法解決有條件並行的問題。咱們能夠在case上打上相關的特性tag,先按照tag進行分組,每一個組內再分批次。這樣,tag組內能夠並行執行,tag組與組之間必須串行執行。

 

 

        二. 總體流程示意圖

       

 

        三. 工具開發中碰到的問題、優化、展望

        問題1: 合併log時,rebot命令不會自動把相關的出錯截圖拷貝到目標路徑

        解決辦法:合併以前,先修改一下每一個批次裏面的output.xml文件,對截圖路徑作一個修正,保持正確的相對路徑便可。

        log存放目錄結構以下:

 

        代碼片斷:

//截圖文件名關鍵字
string eleniumScreenshotKeyword = "selenium-screenshot-";
//讀output.xml文件
string outputContent = File.ReadAllText(Path.Combine(remotePath, "output.xml"), Encoding.UTF8);
//修正截圖路徑爲至關路徑
outputContent = outputContent.Replace(eleniumScreenshotKeyword, string.Format("../remote_{0}/tag_{1}_batch_{2}/", i,tag, j) + eleniumScreenshotKeyword);
//保存output.xml文件
File.WriteAllText(Path.Combine(remotePath, "output.xml"), outputContent, Encoding.UTF8);

 

        問題2: 批次同時執行suite setup會互相干擾

        咱們有時須要在suite執行時先作一個初始化的動做,好比還原數據庫快照,這能夠經過設置suite的suite setup完成。問題是,當咱們把case列表拆成多個批次並行執行時,每一個批次都會執行一遍suite setup,這一樣存在並行干擾的問題。好比:批次A剛還原了數據庫,正在跑case,忽然批次B又把數據庫還原了,那麼這兩個批次之間就互相干擾了。

        解決辦法:將suite setup的事情挪到工具裏面來作,工具能夠控制在適當的時機執行suite setup,好比:第一次執行完畢,還原數據庫,第一次重試,再還原數據庫,第二次重試,再還原數據庫......如此穿插執行。假設我有一個叫「還原數據庫」的關鍵字,如何在工具裏面調它呢?工具只會調case,不會調關鍵字啊。咱們知道,Robot Framework的case和關鍵字資源文件其實都是一些普通的文本文件,是能夠直接編輯的。因而想了一個變通的辦法:修改某個case的內容,在最後追加一個case,隨便起個名字,就叫「RestoreDB」吧,這個case就調用「還原數據庫」關鍵字。這樣,就能夠在工具中執行RestoreDB這個case作數據庫還原了。

代碼片斷:

string restoreDBCaseStatement = "\r\n\r\nRestoreDB\r\n    數據庫還原";
string fileContent = File.ReadAllText(caseFilePath);
//往case文件中增長RestoreDB
File.WriteAllText(caseFilePath, fileContent + restoreDBCaseStatement);

 

        問題3: 執行環境不穩定、掛死

        在實際執行case的過程當中,常常會出現一些莫名其妙的問題,好比:

        1. 跑着跑着IEDriverServer.exe這個進程不起做用了,僵死了

        2. 模態對話框很討厭,特別是沒法預知的模態框,若是不調用confirm action把它消掉的話,即便你在test teardown裏面調close all browsers,也是關不掉IE窗口的,這極有可能會致使case結束了,但IE進程和IEDriverServer.exe進程都還在的情形。

        無論怎麼樣,老是會出現case結束了,但IEDriverServer.exe進程還在的狀況。對於本地執行方式來講,一旦有兩個IEDriverServer.exe進程同時存在的話,那麼執行環境就很不穩定了,什麼奇怪的事情均可能發生;而對於selenium grid環境來講,若是一個case結束了,但IEDriverServer.exe進程還活着的話,會被hub認爲該節點處於繁忙狀態,hub就不會給它分發下一個case,那麼當前批次一直被阻塞,其餘批次也要等待這個批次(線程同步),進而整個執行流程都會被阻塞,後果至關嚴重。

        若是是在本地執行的話,我能夠在case setup裏面調用OperationSystem.Run關鍵字去殺 IEDriverServer.exe進程和IE進程;可是,若是是利用selenium grid分佈式執行的話,這種操做系統級別的關鍵字,是沒法經過hub轉發到節點機器上執行的,由於hub只會轉發webdriver支持的指令,其餘指令都是在本地執行的。

        全部,必需要想辦法保證每一個case執行完畢後,IEDriverServer.exe進程和IE進程都必須被清理掉,不然整個Selenium Grid體系就玩不轉了,這點很重要。

        解決辦法:我也沒想到一個比較優雅的辦法來解決Selenium Grid穩定性的問題,用的一個比較偏門的辦法:

        1. 開發一個WCF服務部署到節點機器,它暴露的URL爲:http://localhost:8732/gridservice/action/killprocess,其做用就是殺IE進程和IEDriverServer進程。

        2. 定義一個關鍵字,叫「安全退出case」,內容以下:

        3. 在case的teardown裏面調用「安全退出case」關鍵字

        這樣作,爲何能達到目的呢?

        1. 首先調用「close all browsers」是爲了殺IEDriverServer進程的(可是有可能IE進程殺不掉,不過不要緊),保證hub會開一個新的session執行後續的那個」open browser「命令。

        2. 」open browser「是selenium庫支持的命令,全部是能夠分發到hub的。

        3. 無論請求分發到哪一個節點,localhost都是指的是節點本機,因此調的是節點本機的服務,固然殺的也是節點本身的進程。

        4. 調用「open browser」訪問WCF服務的URL,相似於自殺式襲擊。由於這個調用會報錯,可是能夠不用管,全部用「Run Keyword And Ignore Error」關鍵字保護起來。襲擊完畢,各類殘餘都被清理的乾乾淨淨(包括IE進程和IEDriverServer進程)。

        這個方案通過測試以後,發現有一個問題:正常狀況下,IEDriverServer.exe進程是在webdriver調用quit方法以後自動退出的,hub與節點之間也是正常的交換信息;而採用自殺式襲擊,來的比較忽然,hub根本不知道節點發生什麼事情,好像變的有點無所適從了,不停的試圖與節點創建聯繫(可是節點又不會正確的通知本身的信息給hub,由於它的進程是非正常終止),直到超時時間到了,才從新和節點創建一個新的session,而後開始後續的執行。hub的默認超時時間是300秒,要等的時間有點久,那麼只好把這個時間設短一點吧,但願它早點超時,早點創建新session。能夠在啓動hub的時候,經過 -timeout 選項設置超時時間。

java -jar selenium-server-standalone-2.39.0.jar -role hub -timeout 20

        通過上述優化以後,實際跑下來效果還能夠,後續還要通過大規模的實際運行來考察這個方案是否成熟穩定。

        對於工具的後續規劃,有兩點考慮:

        1. 開發一個grid的集中管理程序,能夠在一臺機器上統一管理hub、各個節點,包括hub的啓動、關閉、節點與hub的鏈接及斷開,這樣就不用遠程登陸到每臺機器去操做了。

        2. 開發一個調度程序,用於管理整個公司的測試時間安排,但願能夠協調好優先級,均衡的利用grid,不致於一下子扎堆的使用grid,一下子又長時間的讓grid空閒。

相關文章
相關標籤/搜索