目前還未開源源代碼,有需要的同學可以留言,發到大家郵箱。
前言
假設一個場景:在公司或部門裏你們有多條業務線,如果有這麼一個框架,當你告訴它你想在哪種瀏覽器上執行那條業務線的自動化用例後,你可以通過一個適配器類完成你想要的頁面訪問,而且只需要把你寫在配置文件裏面的頁面跟元素identity告訴它,它就會給你返回你需要的element讓你完成你本來需要手工操作的一些行爲(這裏說的identity不是dom裏的元素id,是配置文件中自定義的一個類似數據庫主鍵的東西)。這樣是不是會很便捷? 你不需要再去調研用什麼開源的框架,不需要再去寫一些瀏覽器跟頁面元素 屬性,行爲封裝的類。
你要做的就有兩件事:一是寫配置:把你要操作的頁面跟元素寫到配置文件裏,二是:翻譯手工case,把你手工操作的過程翻譯成自動化,其它任何事情都不需要做!!!這也是爲什麼標題裏要加一個complete的原因
先來個直觀的demo看下我說的這兩步。
配置是這樣的:
<pages>
<page identity="CHomePage" relativeURL="/beijing" name="">
<element identity="searchBox" accessType="" id="b_keyword" name="" xpath=""></element>
<element identity="searchBtn" accessType="" id="" name="" xpath="" className="ser-btn"></element>
</page>
</pages>
手工case翻譯過來是這樣的。
public class Demo { public static void main(String[] args){ PageAdapter.openBrowser("https://passport.xx.com/login"); WebElement userName = PageAdapter.getRuntimeElement("MainCLoginPage", "usernameTextBox"); userName.sendKeys("**"); WebElement passWord = PageAdapter.getRuntimeElement("MainCLoginPage", "passwordUserText") passWord.sendKeys("**"); WebElement signInBtn = PageAdapter.getRuntimeElement("MainCLoginPage", "loginBtn"); signInBtn.click(); } }
開始詳細框架介紹之前,先大概談談自己對UI自動發展歷史的理解,不感興趣可直接跳過。
在移動互聯網之前,大部分互聯網產品都是CS或者BS架構的,當時的開發測試工作主要是接口自動化,工具開發與UI自動化,UI自動化有很多的開源框架,對於剛入行的同學來說,因UI自動化上手簡單且執行可觀性強,其吸引力可見一般。如果你也曾走過這條道,是否現在還清晰的記得當第一次看到自己寫的程序可以打開一個瀏覽器在上面按你指定的方式點點點的時候的那份喜悅。本文因主要講解的是自己寫的一個UI自動化框架,在次不會對於接口自動化做過多的分析。但給看到此文的初學者一點建議,在入行之後,不要被UI自動化偏應用的假象所迷惑,一定要堅持內功修煉,多看一些開源框架的實現,結合自己對於設計模式的理解不斷深入下去,不斷培養自己的架構思維並在垂直的接口自動化及其它領域入行並深入進去。好了,不多說了,現在進入正文。
內容大綱:
1。設計思路
2。核心實現
3。使用方法及應用場景
設計思路:
目前主流的網站,從樣式上大致可分爲兩類:
一種是頁面深度比較淺且頁面模塊比較固定的業務類網站,此類網站大都只有首頁,列表頁,詳情頁這幾大主要頁面。
另一種網站是信息類網站,如論壇,百科這類的網站,此類網站最大的特點就是頁面展示信息量龐大且模塊不固定。當你大腦中有這兩種網站的大概印象之後,我們可以簡單的利用建模的思路去對他們進行抽象,第一種很簡單,比較符合page model的應用場景,每個頁面對象包含對應的元素域。第二種,對於這類網站,大多人很容易選擇的一種方式是採用過程式的方法去根據業務場景實現case。這樣的結果會是大量重複代碼的堆積,case易讀性及可維護性都很差。這種方式我也使用過,但後續這樣寫出來的case基本就是一次性的了,很難維護也很難在測試過程中應用起來。後來去超市買東西的時候,超市的貨架給了我一個靈感,正常我們找東西,是先到對應的商品區,然後再到具體的貨架找到想要的商品。這時候如果我們把影響我們的所有商品都清空,我們能看到的就是一個個的貨架,到此,我們可以對應的抽象出一個模型,商品區對應具體的頁面,貨架對應具體的重複元素(a標籤)的父元素,這時我們嘗試着去用page model去做封裝會發現,這樣的封裝會更簡潔,因爲我們只有頁面,容器類元素及特定元素特徵的封裝。而不會像第一類網站對應的page model那麼複雜,因爲第一類網站我們會把不同頁面的很多不同的元素都封裝到頁面中。
因爲我目前接觸的業務網站是第一種類型,所以我以此類網站的爲例開始下文的具體說明。
Page Model 的封裝如下:
<pages>
<page identity="CHomePage" relativeURL="/beijing" name="">
<element identity="searchBox" accessType="" id="b_keyword" name="" xpath=""></element>
<element identity="searchBtn" accessType="" id="" name="" xpath="" className="ser-btn"></element>
</page>
</pages>
我所在的部門,業務分爲四塊,因此會有不同業務線的不同page的封裝,項目結構如下:
有了個業務線配置之後我們需要一個全局的配置(說明及截圖如下),此配置決定了:
我要測試的業務線(決定加載哪個page model配置文件)
在哪個瀏覽器上跑
瀏覽器的一些基本設置
業務線的base url,此url與page model裏面page的relative url 組成最終的頁面地址,這主要是爲了滿足在不同環境執行case的需求。
<Configuration> <!--runOn 代表測試使用的瀏覽器目前支持firefox,IE 不要隨便寫,代碼裏面會用到這個值來做driver的初始化--> <runOn>firefox</runOn> <!--aut 代表被測業務線,b代表企業端,c代表求職者端 不要隨便寫,代碼裏面會用到這個值來讀取元素配置文件--> <aut>b</aut> <!--baseURL 代表被測業務線的根路徑,與配置文件中的relativeURL拼接形成完整的URL--> <!--<baseURL>http://www.chinahr.com</baseURL>--> <baseURL>http://qy.chinahr.com</baseURL> <driverGroup> <firefox isEnabled="false"> <poolMinIdle>0</poolMinIdle> <poolMaxActive>12</poolMaxActive> <pageLoadTimeout>600</pageLoadTimeout> <implicitWaitTimeout>50</implicitWaitTimeout> <scriptTimeout>50</scriptTimeout> <windowWidth>1024</windowWidth> <windowHeight>768</windowHeight> <userAgent>Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0</userAgent> <!--# userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:45.0) Gecko/20100101 Firefox/45.0--> </firefox> </driverGroup> </Configuration>
再回到page model對應的xml文件的結構
<pages>
<page identity="CHomePage" relativeURL="/beijing" name="">
<element identity="searchBox" accessType="" id="b_keyword" name="" xpath=""></element>
<element identity="searchBtn" accessType="" id="" name="" xpath="" className="ser-btn"></element>
</page>
</pages>
根節點是pages,實際用途就是對應反序列化後最外層的實體類
內層是pages,relative url是對應頁面的相對地址, identity這個需要額外的說明一下,這個很重要,他對應的是最終要生成的page model的class name。
page下面是具體的元素,發現了麼,也有個identity,它對應的是我們page class 內部的field name
元素的屬性包括了常見的發現元素使用的屬性:id,className,xPath,在代碼裏我使用了狀態模式,封裝的ElementFinder類可以根據這些配置的屬性逐一去獲取元素,一旦獲取到就終止獲取操作。element節點還有一個屬性叫findType,正常我們是不配置它的值的,如果配置了ElementFinder就會根據此attribute的值去獲取元素,而不會去使用狀態模式逐一去獲取元素。
selinum這些開源的框架,我們常用的獲取元素的方法如下:
driver.findElements(by)
對於dirver的獲取我們採用工廠模式,根據全局配置去實例化具體的driver並用thread local存儲起來。在實例化driver之前我們會根據全局配置去反序列化對應的page model的配置文件。有了這個內存對象之後,我們可以把頁面,元素以及元素,屬性的對應關係生成出來,大家可以先簡單的理解爲一個複合map。這個map裏面的key 就是我們配置文件裏面配置的 page identity 跟 element identity。所以我們可以通過這兩個identiy 拿到頁面個元素的所有屬性,然後使用狀態模式及selenium 封裝的具體方法去獲取具體的元素。
我們的page model的類名,page model裏元素對應field的name是與配置文件中page與element的identity的值完全一致的,獲取元素我們封裝成統一的方法,其接收兩個參數page identity 跟 element identity,代碼如下:
CHomePage WebPageBase { WebElement searchBoxCHomePage() { } WebElement getsearchBox() { String methodName = Thread.currentThread().getStackTrace()[].getMethodName()String elemetIdentity = CommonUtils.removeGetFromMethodName(methodName)ElementFactory.getElementByPageAndElementIdentity(.identityelemetIdentity)} WebElement getsearchBtn() { String methodName = Thread.currentThread().getStackTrace()[].getMethodName()String elemetIdentity = CommonUtils.removeGetFromMethodName(methodName)ElementFactory.getElementByPageAndElementIdentity(.identityelemetIdentity)} }
到此我們會發現,page model 這個類完全沒有硬編碼的部分,因此我們是可以在運行時通過解析配置文件把它動態的創建出來的,此處使用jdk javaassist的ClassPool,CtClass,CtField etc.
import javassist.*
到此我們需要考慮用戶使用的問題了,現在我們有了:
根據配置創建出來的具體driver
根據配置動態創建出來的page model對象及包含他們的pages對象
除此之外,爲了方便用戶使用,我們把pages跟dirver(瀏覽器)的一些常用方法封裝到一個PageAdapter類裏面。並且暴漏底層對於dirver通用方法的封裝類Dirvers給用戶提供特方法的調用如:執行js操作。
最終在PageAdapter裏面我們獲取元素的方法形式如下:
方法接手的就配置文件中page跟elment的identity
public static WebElement getRuntimeElement(String pageIdentity, String elementIdentity) throws NotFoundException, IllegalAccessException, InstantiationException { try { try { ClassPool.getDefault().getCtClass("com.*.pages").toClass(); } catch (Exception ex) { } cls = Class.forName("com.*.pages").newInstance().getClass(); Method method = getMethodByName(pageIdentity); WebPageBase page = (WebPageBase) method.invoke(null, null); cls = Class.forName("com.*.models." + pageIdentity).newInstance().getClass(); Method elementGetMethod = getMethodByName("get" + elementIdentity); return (WebElement) elementGetMethod.invoke(page, null); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null; }
除此框架還重寫了junit的執行流程,支持自定義的日誌收集及郵件發送報告(報告中可以包括執行失敗的截圖)的功能。
最後上個demo:
用戶只需要按照手動case的方式去把它翻譯成自動化的case。
public class Demo { public static void main(String[] args) throws IllegalAccessException, NotFoundException, InstantiationException { String originMobile = "159****3229"; PageAdapter.openBrowser("https://passport.xx.com/login?path=&PGTID=0d000000-0000-081a-3eb9-09f2e0f1996f&ClickID=1"); WebElement userName = PageAdapter.getRuntimeElement("MainCLoginPage", "usernameTextBox"); userName.sendKeys("**"); WebElement passWord = PageAdapter.getRuntimeElement("MainCLoginPage", "passwordUserText"); passWord.sendKeys("**"); WebElement signInBtn = PageAdapter.getRuntimeElement("MainCLoginPage", "loginBtn"); signInBtn.click(); }
}
使用說明:
使用此框架編寫case,只需要三步:
1. 創建java或者maven工程,引用此框架jar包
2. 創建自己業務對應的page model配置文件
3. 翻譯手動case
工程的結構如下,是不是你的工程一下變得so simple,so tidy
一點總結:
我們做任何東西的初衷肯定是讓它能夠最大限度的滿足我們的需求,之後纔是不斷的優化,讓它在易用性,通用性方面不斷的提高。在框架設計的後期,我們會逐漸發現我們對於底層開源框架的封裝及抽象已經逐漸讓我們不用再去關心他們的具體實現了。大家也可以思考下,此框架是不是可以快速的擴展去支持app ui自動化?需要做哪些工作? 劇透一下,它的第一版是爲了實現app ui自動化,我在老東家寫的。後來有web ui自動化的需求,利用幾天你的時間把它改早了下,產出了現在的這個框架。
最後希望大家看完之後,可以多給提一些寶貴的意見,一起通過技術的碰撞相互學習提高。