WebDriver 封裝
歡迎閱讀WebDriver封裝講義。本篇講義將會(huì)重點(diǎn)介紹Selenium WebDriver API的封裝的概念和方法,以及使用封裝進(jìn)行自動(dòng)化測(cè)試的設(shè)計(jì)。
WebDriver API 封裝
封裝的概念
從之前的講義和學(xué)習(xí)中,我們知道,WebDriver API的調(diào)用以及自動(dòng)化測(cè)試,我們也初步接觸了線性測(cè)試、以及模塊化自動(dòng)化測(cè)試和數(shù)據(jù)驅(qū)動(dòng)測(cè)試,那么回顧之前的內(nèi)容,我們不只是可以利用WebDriver提供的一系列的定位符以便使用元素定位方法,我們這里開(kāi)始嘗試封裝后調(diào)用。首先,我們從封裝的概念開(kāi)始。
封裝是一個(gè)面向?qū)ο缶幊痰母拍?,是面向?qū)ο缶幊痰暮诵膶傩?,通過(guò)將代碼內(nèi)部實(shí)現(xiàn)進(jìn)行密封和包裝,從而簡(jiǎn)化編程。
所謂“對(duì)象”,形象地說(shuō),我們可以把它理解為一塊積木。設(shè)計(jì)積木的人需要設(shè)計(jì)積木的外觀與形狀,還有內(nèi)部的材質(zhì)。堆積木的人對(duì)于內(nèi)部的材質(zhì)并不關(guān)心,他們只需要根據(jù)不同的外觀與形狀來(lái)決定堆放的位置。因此,對(duì)于開(kāi)發(fā)者而言,要設(shè)計(jì)面向?qū)ο蟮某绦?,同時(shí)會(huì)是兩個(gè)迥然不同的身份:設(shè)計(jì)者與使用者。
先談?wù)勈褂谜?。使用者的身份,就是利用已?jīng)提供給你的所有對(duì)象,根據(jù)需求,設(shè)計(jì)出自己需要實(shí)現(xiàn)的程序。就如堆積木的過(guò)程。這恰恰是面向?qū)ο缶幊痰膬?yōu)勢(shì)所在,那就是“對(duì)象的重用”。已經(jīng)設(shè)計(jì)好的對(duì)象,可以被不同的使用者調(diào)用,這些功能既然已經(jīng)實(shí)現(xiàn),對(duì)于使用者而言,當(dāng)然就免去了自己去設(shè)計(jì)的過(guò)程。正如堆積木那樣,既然有了現(xiàn)成設(shè)計(jì)好的積木,使用者所要做的工作就是把這些積木最后組合起來(lái),堆成不同的形狀。WebDriver所提供的類庫(kù),就是這樣的積木。那么我們以下的操作將會(huì)基于上述的定位符進(jìn)行定位操作。
前面說(shuō)到對(duì)象好比是一個(gè)積木,設(shè)計(jì)者需要定義好這個(gè)積木的外觀和形狀,也要考慮積木內(nèi)部的制作,例如選用的材質(zhì),以及是空心還是實(shí)心。如果將這個(gè)積木剖開(kāi)來(lái)看,實(shí)際上該對(duì)象應(yīng)分為內(nèi)、外兩層。由于使用者只關(guān)心外部的實(shí)現(xiàn),因此設(shè)計(jì)者就需要考慮,哪些實(shí)現(xiàn)應(yīng)暴露在外,哪些實(shí)現(xiàn)應(yīng)隱藏于內(nèi)。這就體現(xiàn)了對(duì)象的封裝的思想。
簡(jiǎn)而言之,封裝就是把原始和原生的方法進(jìn)行再包裝。將原始的代碼用心的代碼包裝起來(lái),通過(guò)對(duì)新代碼的調(diào)用,來(lái)使用原始的代碼的過(guò)程。
封裝的好處
對(duì)Selenium進(jìn)行封裝的好處主要有如下三個(gè)方面:
- 使用成本低
- 不需要要求所有的測(cè)試工程師會(huì)熟練使用Selenium,而只需要會(huì)使用封裝以后的代碼
- 不需要對(duì)所有的測(cè)試工程師進(jìn)行完整培訓(xùn)。也避免工作交接的成本。
- 測(cè)試人員使用統(tǒng)一的代碼庫(kù)
- 維護(hù)成本低
- 通過(guò)封裝,在代碼發(fā)生大范圍變化和遷移的時(shí)候,不需要維護(hù)所有代碼,只需要變更封裝的部分即可
- 維護(hù)代碼不需要有大量的工程師,只需要有核心的工程師進(jìn)行封裝的維護(hù)即可
- 代碼安全性
- 對(duì)作為第三方的Selenium進(jìn)行封裝,是代碼安全的基礎(chǔ)。
- 對(duì)于任何的代碼的安全隱患,必須由封裝來(lái)解決,使得風(fēng)險(xiǎn)可控。
- 使用者并不知道封裝內(nèi)部的代碼結(jié)構(gòu)。
封裝的目的
封裝,最終為了實(shí)現(xiàn)自動(dòng)化測(cè)試框架。在自動(dòng)化測(cè)試領(lǐng)域,有一個(gè)經(jīng)典的問(wèn)題:“既然可以編寫或者通過(guò)record & playback可以做一個(gè)腳本,那么為什么還需要自動(dòng)化測(cè)試框架呢?”簡(jiǎn)而言之,就是為什么需要如此這般麻煩的設(shè)計(jì)和編寫自動(dòng)化測(cè)試框架,不是原本已經(jīng)可以做自動(dòng)化測(cè)試了么?
這個(gè)回答可以很“官方”,從維護(hù)性、重用性、安全性和成本等角度可以回答。
在這點(diǎn),就好比是建造房子。在沒(méi)有設(shè)計(jì)框架的基礎(chǔ)上,我們或許可以建造一個(gè)樓房,最多也就三兩層吧;但是對(duì)于一份經(jīng)過(guò)完整設(shè)計(jì)的圖紙,人們可以建造出高樓大廈。
封裝,就是把基礎(chǔ)的石頭等建材,通過(guò)我們的個(gè)性化的方法,將地基可用而安全的搭建好。是自動(dòng)化測(cè)試框架的基石。
自動(dòng)化測(cè)試模型

- 封裝 Selenium 為 BoxDriver
- 在 測(cè)試用例中,實(shí)例化 BoxDriver,產(chǎn)生 bd 對(duì)象
- 使用 bd 對(duì)象,構(gòu)造 業(yè)務(wù)模塊的實(shí)例化對(duì)象,產(chǎn)生 common
- 使用 common 在測(cè)試用例中,構(gòu)建測(cè)試步驟
- 使用數(shù)據(jù)驅(qū)動(dòng)的外部數(shù)據(jù),通過(guò)讀取,進(jìn)行測(cè)試
- 執(zhí)行整個(gè)用例
使用封裝進(jìn)行自動(dòng)化測(cè)試
封裝技術(shù)
- 面向?qū)ο蟮木幊?/li>
- 類:模板,設(shè)計(jì)
- 構(gòu)造方法
- 成員變量
- 成員方法
- 類的實(shí)例化產(chǎn)生對(duì)象
- 實(shí)例化過(guò)程: new 關(guān)鍵字
- 實(shí)例化的方法: 類名(參數(shù))--> 構(gòu)造方法
- 實(shí)例化過(guò)程相當(dāng)于 類執(zhí)行了構(gòu)造方法,產(chǎn)生了 this
- 實(shí)例化出來(lái)的對(duì)象,可以訪問(wèn)成員方法
- 封裝:做一個(gè)模板,這個(gè)模板有webdriver所有的功能
- 封裝 webdriver,自動(dòng)化 WebUI 測(cè)試
- 封裝 Appium,自動(dòng)化 APP UI 測(cè)試
- 封裝 自定義的 Web接口 Driver
- 其他公司的開(kāi)源框架
封裝示例
測(cè)試腳本:以下的腳本
-
找到一個(gè)指定輸入框(selector),并且輸入指定的字符(text)
type(selector, text)
不用在業(yè)務(wù)邏輯中,使用多次的
findElement(By.id(...))public void type(String selector, String text) { WebElement we = this.locateElement(selector); we.clear(); we.sendKeys(text); }?
-
找到一個(gè)可以點(diǎn)擊的元素(selector),并且點(diǎn)擊(click)
click(selector)
public void click(String selector) { this.locateElement(selector).click(); }?
-
找到一個(gè)指定的frame,并且切換進(jìn)去
switchToFrame(selector)
public void switchToFrame(String selector) { WebElement we = this.locateElement(selector); this.baseDriver.switchTo().frame(we); } -
找到一個(gè)指定的select,并且通過(guò)index進(jìn)行選擇
selectByIndex(selector, index)
public void selectByIndex(String selector, int index) { WebElement we = this.locateElement(selector); Select s = new Select(we); s.selectByIndex(index); }?
以上的代碼是封裝了locateElement()的幾種方法,在具體使用封裝過(guò)的代碼的時(shí)候,只需要簡(jiǎn)單的調(diào)用即可。接下來(lái)的重點(diǎn),是介紹 locateElement(selector)的封裝方式。
- 查找元素:
findElement(By...) - 支持各種的查找:8種方式都需要支持,必須通過(guò)
selector顯示出分類-
selector中需要包含一個(gè)特殊符號(hào) - 實(shí)例化 封裝好的類的時(shí)候,需要約定好是什么特殊符號(hào)
- 強(qiáng)制性用
硬編碼 hard code來(lái)實(shí)例化,例如,或者?或者 其他非常用字符=> - 或者,構(gòu)造方法中,傳遞
this.byChar
- 強(qiáng)制性用
-
- 要把查找到元素的返回給調(diào)用的地方:必須要有返回值,類型是
WebElement
private WebElement locateElement(String selector) {
WebElement we;
// 如果定位符中 有 分隔符,那么就從分隔符處分成兩段
// 第一段是By
// 第二段是真正的定位符
// 如果沒(méi)有分隔符,就默認(rèn)用 id 定位
if (!selector.contains(this.byChar)) {
// 用 id 定位
we = this.baseDriver.findElement(By.id(selector));
} else {
// 用 分隔符 分成兩個(gè)部分
String by = selector.split(this.byChar)[0];
String value = selector.split(this.byChar)[1];
we = findElementByChar(by, value);
}
return we;
}
- 接下來(lái)的重點(diǎn),是實(shí)現(xiàn)
findElementByChar(by, value)
private WebElement findElementByChar(String by, String value) {
WebElement we = null;
switch (by.toLowerCase()) {
case "id":
case "i":
we = this.baseDriver.findElement(By.id(value));
break;
case "css_selector":
case "css":
case "cssselector":
case "s":
we = this.baseDriver.findElement(By.cssSelector(value));
break;
case "xpath":
case "x":
we = this.baseDriver.findElement(By.xpath(value));
break;
case "link_text":
case "link":
case "text":
case "linktext":
case "l":
we = this.baseDriver.findElement(By.linkText(value));
break;
//TODO: other by type
}
return we;
}
使用上面的封裝類,就需要指定特定的 selector
| 類型 | 示例(分隔符以逗號(hào),為例) |
描述 |
|---|---|---|
| id | "account" 或者 "i,account" 或者 "id,account" | 分隔符左右兩側(cè)不可以空格 |
| xpath | "x,//*[@id="s-menu-dashboard"]/button/i" | |
| css selector | "s,#s-menu-dashboard > button > i" | |
| link text | "l,退出" | |
| partial link text | "p,退" | |
| name | "n,name1" | |
| tag name | "t,input" | |
| class name | "c,dock-bottom |
調(diào)用示例
void logIn(String account, String password) throws InterruptedException {
BoxDriver driver = this.baseDriver;
driver.type("account", account);
driver.type("password", password);
driver.click("submit");
// 點(diǎn)擊登錄按鈕后,需要等待瀏覽器刷新
Thread.sleep(2000);
}
自動(dòng)化的測(cè)試代碼示例
@Test
public void test01Login() {
common.openPage();
common.logIn(member.getAccount(), member.getPassword());
expectedUrl = this.baseUrl + "sys/index.html";
Assert.assertEquals(driver.getCurrentUrl(), expectedUrl, "新用戶登錄失敗");
common.logOut(lang);
}
封裝需要注意的部分
-
方法有無(wú)返回值、和參數(shù)
-
如果有返回值:確保代碼所有的路徑都會(huì)有返回值
public WebElement getElement(){ if (xxx) { return yyy; } } // 上述代碼會(huì)報(bào)錯(cuò),如果xxx條件不符合,就沒(méi)有返回值的路徑了。 -
如果有參數(shù),確保非值類型的參數(shù)是非空的 不是 null
public void type(Driver driver){ driver.xxx() } // 如果driver是null,那么xxx會(huì)直接報(bào)錯(cuò)。 NullPointerException錯(cuò)誤異常 -
如果有較多的分支語(yǔ)句,需要注意 break的使用
public WebElement getElement(String selector){ switch(selector.toLowerCase()){ case "id": case "i": driver.findElement(By.id(xxx)); break; case "x": xxx } } // 注意需要填寫break在python中,不支持switch語(yǔ)句,python使用的是 if ... elif
def get_element(selector): if selector == "id" or selecor == "i": driver.find_element_by_id(xxx) elif selector == "x": xxx?
-
-
封裝中,WebDriver對(duì)象的傳遞
-
WebDriver應(yīng)該是在封裝的類中,唯一存在。
class AutomateDriver(){ public click(String selector){ WebDriver driver = new FirefoxDriver(); driver.... } public type(String selector, String text){ WebDriver driver = new FirefoxDriver(); driver.... } } // 上述代碼中,會(huì)打開(kāi)多個(gè)火狐瀏覽器 // 正確的做法:聲明一個(gè)全局的類的成員變量,進(jìn)行賦值使用。?
-
關(guān)于Firefox Profile的使用
在做自動(dòng)化測(cè)試的時(shí)候,經(jīng)常會(huì)發(fā)現(xiàn)Firefox 被初始化,提示收藏夾等,遮住元素窗口。因?yàn)镕irefox每次都以默認(rèn)的Profile 加載全新的Firefox,例如如下圖片:

在這樣的情況下,我們需要專門準(zhǔn)備一份獨(dú)立的Firefox Profile進(jìn)行使用。步驟如下
-
在命令行中進(jìn)入Firefox的安裝目錄,然后輸入
firefox -ProfileManager -no-remoteSnap2.jpg -
然后在彈出的窗口中,新建一個(gè)Profile,記錄地址。
e5792a15-e7ed-39fa-bf46-5505c54923a5.png -
根據(jù)剛剛的地址,打開(kāi)Profile所在的文件夾:
C:\Users\Linty\AppData\Roaming\Mozilla\Firefox\ProfilesSnap3.jpg -
復(fù)制
einy9uds.selenium文件夾到base目錄中。Snap4.jpg -
修改代碼
automate_driver.pydef __init__(self, by_char=",", firefox_profile=None): """ 構(gòu)造方法:實(shí)例化 BoxDriver 時(shí)候使用 :param by_char: 分隔符 :param firefox_profile: 可選擇的參數(shù),如果不傳遞,就是None 如果傳遞一個(gè) profile,就會(huì)按照預(yù)先的設(shè)定啟動(dòng)火狐 去掉遮擋元素的提示框等 """ if firefox_profile is not None: firefox_profile = FirefoxProfile(firefox_profile) driver = webdriver.Firefox(firefox_profile=firefox_profile) try: self.base_driver = driver self.by_char = by_char except Exception: raise NameError("Firefox Not Found!") -
修改代碼
xxx_test_cases.pyprofile = "base\\einy9uds.selenium" self.base_driver = AutomateDriver(firefox_profile=profile) 完畢。



