最近正在編寫selenium?webdriver自動化框架,經過幾天的努力,目前基本已經實現了一套即能滿足數據驅動、又能滿足Web關鍵字驅動的自動化框架(主要基于?ant+jenkins+testng+selenium webdriver+jxl實現)。通過這次的自動化框架開發,我深刻的發現了webdriver的強大,甚至我們可以看到阿里巴巴的F2etest瀏覽器兼容性測試平臺也是基于webdriver。以下特別轉載了一篇關于selenium webdriver的介紹,讓我們從深層次理解webdriver:
selenium與webdriver整合后,形成的新的測試工具叫做selenium2.x。在selenium1時間,selenium使用JavaScript來達到測試自動化的目標。
早期的Selenium使用的是javascript注入技術與瀏覽器打交道,需要Selenium RC啟動一個Server,將操作Web元素的API調用轉化為一段段Javascript,在Selenium內核啟動瀏覽器之后注入這段Javascript。開發過Web應用的人都知道,Javascript可以獲取并調用頁面的任何元素,自如的進行操作。由此才實現了Selenium的目的:自動化Web操作。這種Javascript注入技術的缺點是速度不理想,而且穩定性大大依賴于Selenium內核對API翻譯成的Javascript質量高低。
當Selenium2.x 提出了WebDriver的概念之后,它提供了完全另外的一種方式與瀏覽器交互。那就是利用瀏覽器原生的API,封裝成一套更加面向對象的Selenium WebDriver API,直接操作瀏覽器頁面里的元素,甚至操作瀏覽器本身(截屏,窗口大小,啟動,關閉,安裝插件,配置證書之類的)。由于使用的是瀏覽器原生的API,速度大大提高,而且調用的穩定性交給了瀏覽器廠商本身,顯然是更加科學。然而帶來的一些副作用就是,不同的瀏覽器廠商,對Web元素的操作和呈現多少會有一些差異,這就直接導致了Selenium WebDriver要分瀏覽器廠商不同,而提供不同的實現。例如Firefox就有專門的FirefoxDriver,Chrome就有專門的ChromeDriver等等。(甚至包括了AndroidDriver和iOS WebDriver)
WebDriver Wire協議是通用的,也就是說不管是FirefoxDriver還是ChromeDriver,啟動之后都會在某一個端口啟動基于這套協議的Web Service。例如FirefoxDriver初始化成功之后,默認會從http://localhost:7055開始,而ChromeDriver則大概是http://localhost:46350之類的。接下來,我們調用WebDriver的任何API,都需要借助一個ComandExecutor發送一個命令,實際上是一個HTTP request給監聽端口上的Web Service。在我們的HTTP request的body中,會以WebDriver Wire協議規定的JSON格式的字符串來告訴Selenium我們希望瀏覽器接下來做社么事情。
在我們new一個WebDriver的過程中,Selenium首先會確認瀏覽器的native component是否存在可用而且版本匹配。接著就在目標瀏覽器里啟動一整套Web Service,這套Web Service使用了Selenium自己設計定義的協議,名字叫做The WebDriver Wire Protocol。這套協議非常之強大,幾乎可以操作瀏覽器做任何事情,包括打開、關閉、最大化、最小化、元素定位、元素點擊、上傳文件等等等等。
這里筆者初步畫了一個圖來表示各種WebDriver的工作原理:
從上圖中我們可以看出,不同瀏覽器的WebDriver子類,都需要依賴特定的瀏覽器原生組件,例如Firefox就需要一個add-on名字叫webdriver.xpi。而IE的話就需要用到一個dll文件來轉化Web Service的命令為瀏覽器native的調用。另外,圖中還標明了WebDriver Wire協議是一套基于RESTful的web service。
關于WebDriver Wire協議的細節,比如希望了解這套Web Service能夠做哪些事情,可以閱讀Selenium官方的協議文檔, 在Selenium的源碼中,我們可以找到一個HttpCommandExecutor這個類,里面維護了一個Map,它負責將一個個代表命令的簡單字符串key,轉化為相應的URL,因為REST的理念是將所有的操作視作一個個狀態,每一個狀態對應一個URI。所以當我們以特定的URL發送HTTP request給這個RESTful web service之后,它就能解析出需要執行的操作。截取一段源碼如下:
nameToUrl = ImmutableMap.builder()
? ? ? ? .put(NEW_SESSION, post("/session"))
? ? ? ? .put(QUIT, delete("/session/:sessionId"))
? ? ? ? .put(GET_CURRENT_WINDOW_HANDLE, get("/session/:sessionId/window_handle"))
? ? ? ? .put(GET_WINDOW_HANDLES, get("/session/:sessionId/window_handles"))
? ? ? ? .put(GET, post("/session/:sessionId/url"))
? ? ? ? ? ? // The Alert API is still experimental and should not be used.
? ? ? ? .put(GET_ALERT, get("/session/:sessionId/alert"))
? ? ? ? .put(DISMISS_ALERT, post("/session/:sessionId/dismiss_alert"))
? ? ? ? .put(ACCEPT_ALERT, post("/session/:sessionId/accept_alert"))
? ? ? ? .put(GET_ALERT_TEXT, get("/session/:sessionId/alert_text"))
? ? ? ? .put(SET_ALERT_VALUE, post("/session/:sessionId/alert_text"))
? 可以看到實際發送的URL都是相對路徑,后綴多以/session/:sessionId開頭,這也意味著WebDriver每次啟動瀏覽器都會分配一個獨立的sessionId,多線程并行的時候彼此之間不會有沖突和干擾。例如我們最常用的一個WebDriver的API,getWebElement在這里就會轉化為/session/:sessionId/element這個URL,然后在發出的HTTP request body內再附上具體的參數比如by ID還是CSS還是Xpath,各自的值又是什么。收到并執行了這個操作之后,也會回復一個HTTP response。內容也是JSON,會返回找到的WebElement的各種細節,比如text、CSS selector、tag name、class name等等。以下是解析我們說的HTTP response的代碼片段:
try {
? ? ? ? response = new JsonToBeanConverter().convert(Response.class, responseAsText);
? ? ? } catch (ClassCastException e) {
? ? ? ? if (responseAsText != null && "".equals(responseAsText)) {
? ? ? ? ? // The remote server has died, but has already set some headers.
? ? ? ? ? // Normally this occurs when the final window of the firefox driver
? ? ? ? ? // is closed on OS X. Return null, as the return value _should_ be
? ? ? ? ? // being ignored. This is not an elegant solution.
? ? ? ? ? return null;
? ? ? ? }
? ? ? ? throw new WebDriverException("Cannot convert text to response: " + responseAsText, e);
? ? ? } //...
? 相信總結道這里,應該對WebDriver的運行原理應該清楚了!其實挺佩服這一套RESTful web service的設計。感覺封裝WebDriver暴露出來的public API還可以更加友好跟強大一點,這次就先總結道這里,會繼續分析Selenium源碼,繼續分享的!
? ? ? ? 關于使用Selenium WebDriver的經驗小結:
其中WebDriver更加面向對象的方式大大降低了Selenium的入門門檻,對Web元素的操作也非常之簡單易學。實際項目用起來,工作量最大的部分就是你如何解析定位到你的目標項目頁面中的各種元素。好比你要定位一個Button,你可以用ID,可以用CSS,可以用XPATH,你為了點擊這個Button,寫了一個函數調用Selenium里的API,即WebElement里的click()或者submit(),那么另外一個Button怎么辦?成百上千個Button又怎么辦?
所以,你需要有一套自己實現的算法或者封裝,來根據項目頁面的特點提供一套通用的元素定位方式。當你的通用定位邏輯能準確的找到任何一個元素的時候,剩下的事情就順理成章了,交給Selenium WebElement的API就可以了。這一套定位邏輯筆者覺得才是使用Selenium做Web自動化工作量最大的部分。當然有的公司Web項目使用了自己開發的UI框架,例如筆者所在的公司,這樣Web元素的定位規則和算法就比較容易設計。如果Web項目開發出來的頁面代碼比較雜亂無章,那么你就需要更加高明和嚴謹的邏輯去尋找你想要操作和查看的元素了!
在筆者的項目里,筆者自己設計并封裝了一套通用的API,去智能的定位頁面中的各種類型的元素。比如項目里的頁面有大量的dialog和wizard,都是用div+css實現的。我就提供了一個dialog組件,帶有next(),save(),finish(),click(String buttonName),cancel()等方法,然后根據遮罩層和loading Icon的時間來追蹤操作完成的進度。這里只是舉個小小的例子,有機會再分享更多的細節。