概述
在敏捷測(cè)試中UI的自動(dòng)化測(cè)試(一般我們也稱這層測(cè)試為功能測(cè)試或驗(yàn)收測(cè)試,本文單指Web UI的自動(dòng)化測(cè)試)雖然沒(méi)有單元測(cè)試那么廣為提及,但因?yàn)槠渑c最終用戶最近,所以基于用戶場(chǎng)景的UI自動(dòng)化測(cè)試還是有其重要的意義的。使用UI自動(dòng)化測(cè)試對(duì)產(chǎn)品的關(guān)鍵功能路徑進(jìn)行驗(yàn)證及回歸,比起傳統(tǒng)的QA手工執(zhí)行Test case可以更快地得到反饋,也讓發(fā)布變得更有信心。
理想狀況下,我們應(yīng)該將所有可以固化下來(lái)的Test case都自動(dòng)化起來(lái),而讓我們的測(cè)試人員進(jìn)行更有挑戰(zhàn)性的探索性測(cè)試活動(dòng)。讓機(jī)器做已知領(lǐng)域的事兒,讓人對(duì)未知領(lǐng)域進(jìn)行探索。不過(guò)理想歸理想,現(xiàn)實(shí)是殘 酷的。雖然UI層的測(cè)試距離交付最近,但是成本也最高。編寫(xiě)和維護(hù)UI自動(dòng)化測(cè)試需要付出比其他自動(dòng)化測(cè)試更高昂的成本,這也是大多數(shù)團(tuán)隊(duì)放棄UI自動(dòng)化 測(cè)試的主要原因。相比較系統(tǒng)的其他部分,UI是一個(gè)多變的層,如果UI自動(dòng)化測(cè)試沒(méi)有構(gòu)建好,即使界面的一個(gè)微小改動(dòng),整個(gè)測(cè)試集可能就天崩地裂。這也就 是為什么我經(jīng)常對(duì)team里其他人說(shuō):對(duì)于UI自動(dòng)化測(cè)試,可維護(hù)性必須牢記心頭。每當(dāng)你寫(xiě)下一行測(cè)試代碼時(shí),你就必須記住你又給公司添加了一筆成本,而 且這個(gè)成本是持續(xù)增長(zhǎng)的,如果review code的時(shí)候發(fā)現(xiàn)哪條測(cè)試代碼維護(hù)性不好我會(huì)毫不猶豫的刪掉。
或許有人覺(jué)得這有點(diǎn)小題大作,不就UI測(cè)試么,有什么難的。定位元素,然后拿到頁(yè)面元素的值與期望進(jìn)行比較不就可以了。難就難在定位元素上。一般我們會(huì)使用Selenium WebDriver, Watir, Sahi等工具驅(qū)動(dòng)瀏覽器,進(jìn)行元素定位(關(guān)于這些工具的詳細(xì)使用可以參見(jiàn)官方文檔,后文主要以Selenium WebDriver為示例)。這些工具在定位元素上基本上是大同小異:通過(guò)id, name, css, tagName, xpath等方式定位。這些定位方式,從前到后,一個(gè)比一個(gè)不靠譜。比如這個(gè)xpath,好不容易寫(xiě)出個(gè)xpath定位,然后突然有一天前端覺(jué)得某個(gè)地方 不美觀,插入一個(gè)小東西,馬上測(cè)試廢掉。看著這種沒(méi)有改變功能也把功能測(cè)試搞垮掉的現(xiàn)象是不是欲哭無(wú)淚。我有時(shí)天真的在想如果頁(yè)面上每一個(gè)元素都有唯一的 id該多好啊。即使沒(méi)有唯一的id,有name我也可以接受。不過(guò)這一切在遇到ExtJS之后都變了。
遭遇ExtJS
ExtJS是一個(gè)非常霸道的前端框架。使用ExtJS后,頁(yè)面上幾乎所有的一切都被ExtJS接管。盡管互聯(lián)網(wǎng)提 供給用戶的系統(tǒng)鮮有使用ExtJS,但是對(duì)于后臺(tái)系統(tǒng)使用ExtJS確實(shí)帶來(lái)了一些便利。使用ExtJS的基本組件就能組裝出一個(gè)看起來(lái)還不錯(cuò),功能強(qiáng)大 的應(yīng)用。但是ExtJS非常霸道,被他接管后頁(yè)面的生成基本上就是個(gè)黑盒子,而為了在各個(gè)瀏覽器的兼容它在各瀏覽器上生成的html還不一樣。更可恨的是 默認(rèn)情況下它給元素提供的id都是動(dòng)態(tài)生成的。
在剛選擇這個(gè)ExtJS的系統(tǒng)作為我們自動(dòng)化測(cè)試的第一個(gè)試點(diǎn)時(shí),我還有點(diǎn)暗暗高興。比 起那些提供給普通用戶使用的豐富多彩的前端來(lái)說(shuō),這些后臺(tái)系統(tǒng)大多中規(guī)中矩,使用ExtJS后更是層次分明。而且后臺(tái)系統(tǒng)UI的變動(dòng)也不會(huì)太過(guò)于頻繁,我 想或許這個(gè)系統(tǒng)很容易測(cè)試吧。
后來(lái)我看到同事代碼里出現(xiàn):
webDriver.findElement(By.id("ext-gen-1306")) |
我還在想,我們的前端同學(xué)真有“創(chuàng)意”,還用這么隨機(jī)的名字啊。后來(lái)厄運(yùn)來(lái)了,我check out代碼在我這里死活通過(guò)不了。Selenium報(bào)告找不到指定元素。不是吧,我可是使用id進(jìn)行定位的啊。通過(guò)翻閱ExtJS的文檔發(fā)現(xiàn),原來(lái)類(lèi)似 ext-gen-xxx這類(lèi)id都是ExtJS動(dòng)態(tài)生成的。好吧,我使用name進(jìn)行定位吧,后來(lái)發(fā)現(xiàn)很多元素居然沒(méi)有name屬性。再來(lái)看看ExtJS 生成的html,基本上把通過(guò)xpath進(jìn)行定位的路給堵死了。要了解ExtJS生成的html,可以去ExtJS官方查看一些Demo。
曙光
閱讀ExtJS文檔我們發(fā)現(xiàn),ExtJS極其強(qiáng)調(diào)它的組件模型。而用ExtJS寫(xiě)的前端代碼也呈現(xiàn)出很好的結(jié)構(gòu)。因?yàn)橹霸鴱氖逻^(guò)ASP.NET的開(kāi) 發(fā),我想是不是可以使用ASP.NET類(lèi)似的方式先編寫(xiě)一些小控件類(lèi),這些類(lèi)對(duì)ExtJS的基本組件進(jìn)行包裝。然后利用這些小控件類(lèi)組裝出一個(gè)個(gè)頁(yè)面。這 樣不僅能把單個(gè)元素的定位分散到單個(gè)控件類(lèi)里,而且可以做到極大程度的復(fù)用。在傳統(tǒng)的UI自動(dòng)化測(cè)試中我們使用Page Object模式來(lái)封裝一個(gè)個(gè)頁(yè)面,但是對(duì)于ExtJS來(lái)講頁(yè)面的粒度還顯得過(guò)大。如是模仿ASP.NET的控件模型,我創(chuàng)建了Control, Button, TextBox等一系列基本的控件類(lèi)。而原來(lái)Page Object中的Page不再使用WebDriver直接定位元素了,我們通過(guò)這些基本控件組裝頁(yè)面。
實(shí)現(xiàn)
在這里我用一個(gè)簡(jiǎn)單的用戶登錄作為例子:
Control是我們的基本類(lèi)型,所有的控件包括頁(yè)面都從這個(gè)類(lèi)派生。
Control只提供了很少幾個(gè)方法:
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是一個(gè)非常重要的方法,這在后面會(huì)介紹。
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; } } |
所有的可以包含其他控件的類(lèi)型都從CompositeControl派生,包括Page。比如下面的Window就是這類(lèi)元素:
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); } } |
下面是一個(gè)基本控件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提供了一個(gè)query接口,我們可以利用這個(gè)接口傳入一些查詢表達(dá)式查詢到頁(yè)面上的Ext控件,而這里的getQuery就是每個(gè)控件的查詢表達(dá)式吧。因?yàn)轫?yè)面上的ExtJS控件是層次的,所以我們可以利用這種嵌套關(guān)系進(jìn)行精確的定位。
好了,來(lái)看看我們的登陸頁(yè)面如何封裝吧:
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沒(méi)有提供代碼,都很簡(jiǎn)單可以自行進(jìn)行封裝一下(熟悉ASP.NET的同學(xué)可能對(duì)這里代碼有點(diǎn)眼熟)。
按照這種思路,只要我們封裝好所有的基本ExtJS控件,對(duì)于所有的頁(yè)面我們剩下的工作就是組裝的工作了。在完成這些之后,我甚至發(fā)現(xiàn)使用 ExtJS的應(yīng)用比那些沒(méi)有使用ExtJS的應(yīng)用更容易進(jìn)行測(cè)試。在這里我們只需要完善我們的基本控件封裝就可以讓我們的測(cè)試更佳穩(wěn)固,而對(duì)于編寫(xiě)測(cè)試的 人來(lái)說(shuō)只需要集中精力關(guān)注Test case。