概述
在敏捷測試中UI的自動化測試(一般我們也稱這層測試為功能測試或驗收測試,本文單指Web UI的自動化測試)雖然沒有單元測試那么廣為提及,但因為其與最終用戶最近,所以基于用戶場景的UI自動化測試還是有其重要的意義的。使用UI自動化測試對產品的關鍵功能路徑進行驗證及回歸,比起傳統的QA手工執行Test case可以更快地得到反饋,也讓發布變得更有信心。
理想狀況下,我們應該將所有可以固化下來的Test case都自動化起來,而讓我們的測試人員進行更有挑戰性的探索性測試活動。讓機器做已知領域的事兒,讓人對未知領域進行探索。不過理想歸理想,現實是殘 酷的。雖然UI層的測試距離交付最近,但是成本也最高。編寫和維護UI自動化測試需要付出比其他自動化測試更高昂的成本,這也是大多數團隊放棄UI自動化 測試的主要原因。相比較系統的其他部分,UI是一個多變的層,如果UI自動化測試沒有構建好,即使界面的一個微小改動,整個測試集可能就天崩地裂。這也就 是為什么我經常對team里其他人說:對于UI自動化測試,可維護性必須牢記心頭。每當你寫下一行測試代碼時,你就必須記住你又給公司添加了一筆成本,而 且這個成本是持續增長的,如果review code的時候發現哪條測試代碼維護性不好我會毫不猶豫的刪掉。
或許有人覺得這有點小題大作,不就UI測試么,有什么難的。定位元素,然后拿到頁面元素的值與期望進行比較不就可以了。難就難在定位元素上。一般我們會使用Selenium WebDriver, Watir, Sahi等工具驅動瀏覽器,進行元素定位(關于這些工具的詳細使用可以參見官方文檔,后文主要以Selenium WebDriver為示例)。這些工具在定位元素上基本上是大同小異:通過id, name, css, tagName, xpath等方式定位。這些定位方式,從前到后,一個比一個不靠譜。比如這個xpath,好不容易寫出個xpath定位,然后突然有一天前端覺得某個地方 不美觀,插入一個小東西,馬上測試廢掉。看著這種沒有改變功能也把功能測試搞垮掉的現象是不是欲哭無淚。我有時天真的在想如果頁面上每一個元素都有唯一的 id該多好啊。即使沒有唯一的id,有name我也可以接受。不過這一切在遇到ExtJS之后都變了。
遭遇ExtJS
ExtJS是一個非常霸道的前端框架。使用ExtJS后,頁面上幾乎所有的一切都被ExtJS接管。盡管互聯網提 供給用戶的系統鮮有使用ExtJS,但是對于后臺系統使用ExtJS確實帶來了一些便利。使用ExtJS的基本組件就能組裝出一個看起來還不錯,功能強大 的應用。但是ExtJS非常霸道,被他接管后頁面的生成基本上就是個黑盒子,而為了在各個瀏覽器的兼容它在各瀏覽器上生成的html還不一樣。更可恨的是 默認情況下它給元素提供的id都是動態生成的。
在剛選擇這個ExtJS的系統作為我們自動化測試的第一個試點時,我還有點暗暗高興。比 起那些提供給普通用戶使用的豐富多彩的前端來說,這些后臺系統大多中規中矩,使用ExtJS后更是層次分明。而且后臺系統UI的變動也不會太過于頻繁,我 想或許這個系統很容易測試吧。
后來我看到同事代碼里出現:
webDriver.findElement(By.id("ext-gen-1306")) |
我還在想,我們的前端同學真有“創意”,還用這么隨機的名字啊。后來厄運來了,我check out代碼在我這里死活通過不了。Selenium報告找不到指定元素。不是吧,我可是使用id進行定位的啊。通過翻閱ExtJS的文檔發現,原來類似 ext-gen-xxx這類id都是ExtJS動態生成的。好吧,我使用name進行定位吧,后來發現很多元素居然沒有name屬性。再來看看ExtJS 生成的html,基本上把通過xpath進行定位的路給堵死了。要了解ExtJS生成的html,可以去ExtJS官方查看一些Demo。
曙光
閱讀ExtJS文檔我們發現,ExtJS極其強調它的組件模型。而用ExtJS寫的前端代碼也呈現出很好的結構。因為之前曾從事過ASP.NET的開 發,我想是不是可以使用ASP.NET類似的方式先編寫一些小控件類,這些類對ExtJS的基本組件進行包裝。然后利用這些小控件類組裝出一個個頁面。這 樣不僅能把單個元素的定位分散到單個控件類里,而且可以做到極大程度的復用。在傳統的UI自動化測試中我們使用Page Object模式來封裝一個個頁面,但是對于ExtJS來講頁面的粒度還顯得過大。如是模仿ASP.NET的控件模型,我創建了Control, Button, TextBox等一系列基本的控件類。而原來Page Object中的Page不再使用WebDriver直接定位元素了,我們通過這些基本控件組裝頁面。
實現
在這里我用一個簡單的用戶登錄作為例子:
Control是我們的基本類型,所有的控件包括頁面都從這個類派生。
Control只提供了很少幾個方法:
public abstract class Control { protected WebDriver webDriver;
protected Control parent;
public Control(WebDriver webDriver) { this.webDriver = webDriver; }
public String getQuery() { return StringUtils.EMPTY; }
public String getId() { JavascriptExecutor executor = (JavascriptExecutor) webDriver; return (String) executor.executeScript("return " + this.getQuery() + ".id"); } } |
在這里getQuery是一個非常重要的方法,這在后面會介紹。
public abstract class CompositeControl extends Control { protected List <Control> children;
public CompositeControl(WebDriver webDriver) { super(webDriver); children = new ArrayList<Control> (); }
public void addChild(Control control) { this.children.add(control); control.parent = this; } } |
所有的可以包含其他控件的類型都從CompositeControl派生,包括Page。比如下面的Window就是這類元素:
public class Window extends CompositeControl { private String title;
public Window(String title,WebDriver webDriver) { super(webDriver); this.title = title; }
@Override public String getQuery(){ return String.format("Ext.ComponentQuery.query(\"window[title='%s']\")[0]",title); } } |
下面是一個基本控件Button的封裝:
public class Button extends Control { private String text;
public Button(String text, WebDriver webDriver) { super(webDriver); this.text = text; }
@Override public String getQuery() { return this.parent.getQuery() + String.format(".query(\"button[text='%s']\")[0]", text); }
public void click() { webDriver.findElement(By.id(getId())).click(); } } |
ExtJS提供了一個query接口,我們可以利用這個接口傳入一些查詢表達式查詢到頁面上的Ext控件,而這里的getQuery就是每個控件的查詢表達式吧。因為頁面上的ExtJS控件是層次的,所以我們可以利用這種嵌套關系進行精確的定位。
好了,來看看我們的登陸頁面如何封裝吧:
public class LoginPage extends ExtJSPage{ public LoginPage(WebDriver webDriver){ super(webDriver); }
private TextBox txtUserName; private TextBox txtPassword; private Button btnLogin; @Override protected void init(){ txtUserName = new TextBox("userName", webDriver); txtPassword = new TextBox("password", webDriver); btnLogin = new Button("登錄", webDriver);
Window win = new Window("登陸", webDriver); win.addChild(txtUserName); win.addChild(txtPassword); win.addChild(btnLogin);
this.addChild(win); }
public void login(String userName, String password){ txtUserName.setValue(userName); txtPassword.setValue(password); btnLogin.click(); } } |
上面的TextBox和ExtJSPage沒有提供代碼,都很簡單可以自行進行封裝一下(熟悉ASP.NET的同學可能對這里代碼有點眼熟)。
按照這種思路,只要我們封裝好所有的基本ExtJS控件,對于所有的頁面我們剩下的工作就是組裝的工作了。在完成這些之后,我甚至發現使用 ExtJS的應用比那些沒有使用ExtJS的應用更容易進行測試。在這里我們只需要完善我們的基本控件封裝就可以讓我們的測試更佳穩固,而對于編寫測試的 人來說只需要集中精力關注Test case。