基于 SELENIUM 的自動化測試架構
非常感謝各位查閱本篇文章,筆者在此感謝各位。
目前市面上有分門別類的自動化測試工具,這篇文章將討論開源自動化測試工具 Selenium
的使用,以及圍繞該工具進行自動化測試的理念、方案以及測試架構的構建。
1. 工具的使用
1.1 Selenium 介紹
Selenium
是開源的自動化測試工具,它主要是用于Web 應用程序的自動化測試,不只局限于此,同時支持所有基于web 的管理任務自動化。
Selenium
官網的介紹如下:
Selenium is a suite of tools to automate web browsers across many platforms.
- runs in many browsers and operatingsystems
- can be controlled by many programming languages and testing frameworks.
Selenium 是用于測試 Web 應用程序用戶界面 (UI) 的常用框架。它是一款用于運行端到端功能測試的超強工具。您可以使用多個編程語言編寫測試,并且 Selenium 能夠在一個或多個瀏覽器中執(zhí)行這些測試。
Selenium 經歷了三個版本:Selenium 1,Selenium 2 和 Selenium 3。Selenium 也不是簡單一個工具,而是由幾個工具組成,每個工具都有其特點和應用場景。
Selenium 誕生于 2004 年,當在 ThoughtWorks 工作的 Jason Huggins 在測試一個內部應用時。作為一個聰明的家伙,他意識到相對于每次改動都需要手工進行測試,他的時間應該用得更有價值。他開發(fā)了一個可以驅動頁面進行交互的 Javascript 庫,能讓多瀏覽器自動返回測試結果。那個庫最終變成了 Selenium 的核心,它是 Selenium RC(遠程控制)和 Selenium IDE 所有功能的基礎。Selenium RC 是開拓性的,因為沒有其他產品能讓你使用自己喜歡的語言來控制瀏覽器。這就是 Selenium 1。
然而,由于它使用了基于 Javascript 的自動化引擎,而瀏覽器對 Javascript 又有很多安全限制,有些事情就難以實現(xiàn)。更糟糕的是,網站應用正變得越來越強大,它們使用了新瀏覽器提供的各種特性,都使得這些限制讓人痛苦不堪。
在 2006 年,一名 Google 的工程師, Simon Stewart 開始基于這個項目進行開發(fā),這個項目被命名為 WebDriver。此時,Google 早已是 Selenium 的重度用戶,但是測試工程師們不得不繞過它的限制進行工具。Simon 需要一款能通過瀏覽器和操作系統(tǒng)的本地方法直接和瀏覽器進行通話的測試工具,來解決Javascript 環(huán)境沙箱的問題。WebDriver 項目的目標就是要解決 Selenium 的痛點。
到了 2008 年,Selenium 和 WebDriver 兩個項目合并。Selenium 有著豐富的社區(qū)和商業(yè)支持,但 WebDriver 顯然代表著未來的趨勢。兩者的合并為所有用戶提供了一組通用功能,并且借鑒了一些測試自動化領域最閃光的思想。這就是 Selenium 2。
2016 年,Selenium 3 誕生。移除了不再使用的 Selenium 1 中的 Selenium RC,并且官方重寫了所有的瀏覽器驅動。
1.2 Selenium 工具集
-
Selenium IDE
Selenium IDE (集成開發(fā)環(huán)境) 是一個創(chuàng)建測試腳本的原型工具。它是一個 Firefox 插件,實現(xiàn)簡單的瀏覽器操作的錄制與回放功能,提供創(chuàng)建自動化測試的建議接口。Selenium IDE 有一個記錄功能,能記錄用戶的操作,并且能選擇多種語言把它們導出到一個可重用的腳本中用于后續(xù)執(zhí)行。
-
Selenium RC
Selenium RC 是selenium 家族的核心工具,Selenium RC 支持多種不同的語言編寫自動化測試腳本,通過selenium RC 的服務器作為代理服務器去訪問應用從而達到測試的目的。
selenium RC 使用分Client Libraries 和Selenium Server。
- Client Libraries 庫主要主要用于編寫測試腳本,用來控制selenium Server 的庫。
- Selenium Server 負責控制瀏覽器行為,總的來說,Selenium Server 主要包括3 個部分:Launcher、Http Proxy、Core。
-
Selenium Grid
Selenium Grid 使得 Selenium RC 解決方案能提升針對大型的測試套件或者哪些需要運行在多環(huán)境的測試套件的處理能力。Selenium Grid 能讓你并行的運行你的測試,也就是說,不同的測試可以同時跑在不同的遠程機器上。這樣做有兩個有事,首先,如果你有一個大型的測試套件,或者一個跑的很慢的測試套件,你可以使用 Selenium Grid 將你的測試套件劃分成幾份同時在幾個不同的機器上運行,這樣能顯著的提升它的性能。同時,如果你必須在多環(huán)境中運行你的測試套件,你可以獲得多個遠程機器的支持,它們將同時運行你的測試套件。在每種情況下,Selenium Grid 都能通過并行處理顯著地縮短你的測試套件的處理時間。
-
Selenium WebDriver
WebDriver 是 Selenium 2 主推的工具,事實上WebDriver是Selenium RC的替代品,因為Selenium需要保留向下兼容性的原因,在 Selenium 2 中, Selenium RC才沒有被徹底的拋棄,如果使用Selenium開發(fā)一個新的自動化測試項目,那么我們強烈推薦使用Selenium2 的 WebDriver進行編碼。另外, 在Selenium 3 中,Selenium RC 被移除了。
1.3 Selenium WebDriver 的使用
接下來的內容,我們將會主要討論本文的核心重點, Selenium WebDriver 的使用。 Selenium WebDriver 是從 Selenium 2 開始使用并流行, 在 Selenium 3 中得到進一步發(fā)展的工具,是當前 Selenium 的最核心的工具。WebDriver 具有清晰面向對象 API,能以最佳的方式與瀏覽器進行交互。
Selenium WebDriver 就好比是一個懂瀏覽器的司機,它可以在瀏覽器的網頁上行走,走到網頁內容的任何地方,可以參觀網頁的任何地方,并且和網頁進行交互。那么作為測試工程師,如果想和這樣的一個司機打交道,就必須要掌握和這樣的司機打交道的技能。
- 學習司機會使用的語言,并使用該語言,以及合適的溝通工具與司機進行交流
- Java
- Python
- C#
- JavaScript
- PHP
- Ruby
- 給司機找到合適的瀏覽器,以便司機在瀏覽器上行走。
- 支持多種瀏覽器,包括 Chrome,F(xiàn)irefox,IE,Edge,Safari,Opera 等
Selenium WebDriver 的使用主要分為兩個場景:
- 懂瀏覽器的司機,WebDriver 類
- 用 WebDriver 提供的模板,制造一個司機。
- WebDriver 的第一個應用場景,就是這個司機的各種能力,包括但不限于以下的部分
- 用瀏覽器打開指定的 URL
- 清理瀏覽器的Cookie
- 在瀏覽器中尋找頁面元素(Web Element)
- 查找單個的指定元素
- 查找一組有共同屬性的元素,并進行遍歷等。
- 控制瀏覽器的基本操作:
- 前進: forward()
- 后退: backward()
- 刷新: refresh()
- 關閉: close()
- 最大化: maximize_window()
- 彈窗: switch_to_alert()
- 返回瀏覽器的屬性
- current_url
- title
- 執(zhí)行 JavaScript 腳本
- 在瀏覽器中找到的元素,WebElement 類
- 司機在瀏覽器中找到頁面元素以后,對它做的任何操作,都是 WebDriver 的第二個主要的場景
- 點擊該元素: click()
- 清除該元素原有的文字: clear()
- 給該元素填寫新的文字: send_keys()
- 獲取該元素的文字: text
- 獲取該元素的指定屬性: get_attribute()
- 對該元素進行二次加工
- 構成 frame 并切換進去: switch_to.frame(元素)
- 構成 select 并進行操作: Select(元素).select_by_value()
- 司機在瀏覽器中找到頁面元素以后,對它做的任何操作,都是 WebDriver 的第二個主要的場景
1.4 Selenium 環(huán)境搭建
Selenium 的環(huán)境搭建基本上分為三個部分:
- 安裝編程語言以及IDE(集成編程環(huán)境),用來操作 WebDriver
- 安裝 Selenium WebDriver,實現(xiàn)瀏覽器的測試
- 安裝瀏覽器,和指定的驅動,完成自動化測試的執(zhí)行
接下來分別用目前市面上主流的 Java 和 Python 環(huán)境進行搭建。
-
Java 版本
安裝 Java 語言,即 JDK。推薦 1.8 的版本。
安裝 IDE,推薦 JetBrains IDEA Community Edition,這款是目前主流的 Java 開發(fā)工具,而且社區(qū)版是免費使用的,擁有出色的用戶交互,以及使用界面,完全能夠應對一般的自動化測試的編程。當然如果你更加熟悉Eclipse,也是可以使用的。
-
安裝 Selenium,推薦使用 Maven 直接引入依賴。當自動化測試作為團隊共同的解決方案,而不是一個人單獨維護的方案的時候,團隊需要統(tǒng)一的 Selenium 版本以及共同的 API 操作,Maven 的使用,無疑簡化了項目的難度,很好的解決了此類問題。
<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>3.3.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-remote-driver --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-remote-driver</artifactId> <version>3.3.1</version> </dependency>
當然,你也可以直接下載 Selenium Standalone Server,并且手工引用
Jar 包
到項目中。最新版3.3.1的下載地址:3.3.1 安裝 瀏覽器和瀏覽器的驅動。
以上的步驟,便完成了 Java + Selenium 的環(huán)境搭建。
-
Python 版本
-
安裝 Python 語言。
Python 目前并行了兩套版本,2.x 和 3.x。如果你之前沒有 Python 的使用經驗,建議使用 Python 3.x 版本。兩套版本互相不兼容,并且 Python 從 3.5(含)開始,不再支持 Windows XP 系統(tǒng),請注意。
安裝 Python IDE,推薦 JetBrains Pycharm Community Edition。
-
安裝 Selenium,推薦使用 pip 的方式直接安裝。在命令行下,直接輸入:
# 在Selenium 3 發(fā)布之前,可以用下面命令直接裝selenium # -U = --upgrade 升級安裝 # 自動安裝最新版 # 目前3.0發(fā)布以后,這個命令直接安裝 3.3.1 的最新版 pip install -U selenium # 如果要裝2.53.6版本 pip install selenium==2.53.6
如果你處于沒有外網的情況下,可以采用源碼解壓安裝,前往https://pypi.python.org/pypi/selenium下載最新版的PyPI版本的Selenium,解壓后執(zhí)行:
python setup.py install
-
安裝 瀏覽器和瀏覽器的驅動。
以上的步驟,便完成了 Python + Selenium 的環(huán)境搭建。
-
1.5 Selenium 編程
通過前面的介紹,我們知道 Selenium 支持多種語言,并且推薦使用面向對象的方式進行編程。接下來我們將著重介紹如何使用面向對象的方式進行編程。
在面向對象的理念看來,任何的編碼,都是由對象而來的,這里也不例外。和之前介紹 WebDriver 時候的描述對應,我們需要用到兩種主要的類,并將其實例化。
- WebDriver 類:主要靠直接實例化該類為對象,然后用其對象直接調用該類的方法和屬性
- WebElement 類:主要通過 WebDriver 類實例化的對象,通過對頁面元素的查找,得到 WebElement 類的對象,然后調用該類的方法和屬性。
具體的使用如下,以 Java 語言 和 火狐瀏覽器為例
// 聲明 Web司機,司機是一個火狐類的對象
// 需要用 new 關鍵字來實例化對象, () 代表構造方法
WebDriver driver = new FirefoxDriver();
// Web司機去打開網站
driver.get("http://demo.ranzhi.org");
// 線程停止 3000 毫秒,使得 Web司機有足夠的時間打開網址
Thread.sleep(3000);
// 選擇 用戶名 密碼 并依次輸入 demo 和 demo (用戶名和密碼都是 demo)
weAccount = driver.findElement(By.cssSelector("#account"));
weAccount.clear();
weAccount.sendKeys("demo");
wePassword = driver.findElement(By.cssSelector("#password"));
wePassword.clear();
wePassword.sendKeys("demo");
// 選擇 登錄 按鈕,并點擊 click
driver.findElement(By.cssSelector("#submit")).click();
Thread.sleep(5000);
上述代碼中,使用了一個 WebDriver 類 的對象,即第3行,聲明了該類的對象,并賦值給變量 driver,接著變量 driver 作為 WebDriver 類的對象,使用了多個 WebDriver 類的方法。
- get(url): 第6行,打開網址
- findElement(by, selector): 第12、16、21行都使用了該方法,同時通過對該方法的調用,分別各產生了一個 WebElement類的對象,
weAccount
,wePassword
和最后一個匿名的對象,并通過產生的三個對象,調用 WebElement 類的方法- clear():清理頁面元素中的文字
- sendKeys(text):給頁面元素中,輸入新的文字
- click():鼠標左鍵點擊頁面元素
正是通過這樣的面向對象的方式,產生 Web司機(WebDriver類的對象),并且通過 Web司機不懈的努力,尋找到各種 Web元素(WebElement類的對象)進行操作,這樣便實現(xiàn)了 Selenium WebDriver 作為一款出色的瀏覽器測試工具,進行瀏覽器UI界面的自動化測試的代碼編寫和用例執(zhí)行。
上述代碼,也同樣可是使用 Python 作為編程語言進行操作,如下所示:
# 聲明一個司機,司機是個Firefox類的對象
driver = webdriver.Firefox()
# 讓司機加載一個網頁
driver.get("http://demo.ranzhi.org")
# 給司機3秒鐘去打開
sleep(3)
# 開始登錄
# 1. 讓司機找用戶名的輸入框
we_account = driver.find_element_by_css_selector('#account')
we_account.clear()
we_account.send_keys("demo")
# 2. 讓司機找密碼的輸入框
we_password = driver.find_element_by_css_selector('#password')
we_password.clear()
we_password.send_keys("demo")
# 3. 讓司機找 登錄按鈕 并 單擊
driver.find_element_by_css_selector('#submit').click()
sleep(3)
常用的重點編程對象有如下幾種:
-
WebDriver 類
- get(url): 打開web頁面
- findElement(by, selector): 查找一個頁面元素
- 配合瀏覽器的開發(fā)者工具(推薦 Chrome Developer Tools),有8中方式定位元素:
- id:元素標簽的 id
- css selector:元素標簽的 selector
- xpath:元素標簽的 XPath
- link text:元素標簽的完整文字
- name:元素標簽的 name
- class name:元素標簽的 class name
- tag name:元素標簽的 tag name
- partial link text:元素標簽的部分文字
- 配合瀏覽器的開發(fā)者工具(推薦 Chrome Developer Tools),有8中方式定位元素:
findElements(by, selector):查找一組具有同一屬性的頁面元素,方式同上。
deleteAllCookies():清理 Cookies
executeJs(js):執(zhí)行 JavaScript
quit(): 退出瀏覽器
getTitle():當前瀏覽器的標題屬性
getCurrentUrl():獲取當前瀏覽器的 URL
-
WebElement 類
click():點擊改元素
clear():清除當前的文本
sendKeys(text):給當前的元素輸入文字
getAttribute(attribute):獲取當前元素的某一種屬性
getText():獲取當前元素的文字屬性
isDisplayed():獲取當前元素的 Displayed 屬性
-
Select:針對
<select>
元素進行的操作selectByValue(value)
-
selectByIndex(index)
// 找到該 <select> 元素 we WebElement we = driver.findElement(by, selector); // 使用該元素,實例化一個 Select 類的對象 s Select s = new Select(we); s.selectByValue(value); // 或者用index s.selectByIndex(value)
-
鼠標事件,有關鼠標的操作,不只是單擊,有時候還要做右擊、雙擊、拖動等操作。這些操作包含在ActionChains類中。
- contextClick():右擊
- douchClick():雙擊
- dragAndDrop():拖拽
- moveToElement():鼠標停在一個元素上
- clickAndHold():按下鼠標左鍵在一個元素上
-
Frame: 針對
<iframe>
元素標簽進行的操作很多的頁面中,都包含有內聯(lián)框架(iframe),那么如果需要獲取到其內部的元素并進行操作,必須首先切換到該內聯(lián)框架中,當操作完成以后,再退出到最外層的網頁中
// 找到該內聯(lián)框架的元素 WebElement we = driver.findElement(by, selector); // 利用WebDriver 的對象driver,切換到該內聯(lián)框架中 driver.switchTo().frame(we); // TODO: 進行各種操作 // 退出該內聯(lián)框架,返回到外層的網頁中 driver.switchTo().defaultContent();
?
2. 理念與方案
在第一部分,工具的使用中,我們重點介紹了 Selenium 工具的編程,但是這樣其實對于自動化測試來講,還遠遠不夠。自動化測試的重點,其實依舊是測試用例的編寫和執(zhí)行,要求代碼中,具備測試用例的屬性;同時要求測試的代碼能夠很好的組織起來,通過抽取和分離的理念,實現(xiàn)良好的測試。主要達到以下的幾個目的:
-
具備測試用例的屬性
測試代碼,可以輕松的具備測試用例的屬性,主要包括測試前置條件、清理操作、和斷言(檢查)。
-
避免重復代碼的編寫和復制
通過模塊化拆分頁面功能,避免 WebDriver類的重復實例化和調用,也避免同樣的測試步驟,多次的編寫和復制
-
測試數(shù)據(jù)單獨存放
測試代碼中不需要包含需要輸入的測試數(shù)據(jù),而是把測試數(shù)據(jù)單獨存放在 文本文件,或者數(shù)據(jù)庫中。
-
封裝底層的測試工具
對 Selenium WebDriver 這種第三方的工具,進行封裝起來,避免代碼中直接調用
-
必須使用源代碼管理工具
無論是否是
一人團隊
,源代碼管理工具的使用都是積極地和必要的,推薦使用 Git 。
接下來的描述,將會對上述的理念依次進行講解,實現(xiàn)自動化測試的方案。
2.1 使用單元測試框架
在第一部分,我們對 Selenium WebDriver 的使用,僅僅停留在讓網頁自動的進行操作的階段,并沒有對任何一個步驟進行“檢查”。當然,這樣沒有“檢查”的操作,實際上是沒有測試意義的。那么第一項,我們需要解決的便是“檢查”的問題。
所謂“檢查”,實際上就是斷言。對需要檢查的步驟操作,通過對預先設置的期望值,和執(zhí)行結果的實際值之間的對比,得到測試的結果。在這里,我們并不需要單獨的寫 if
語句進行各種判定,而是可以使用編程語言中對應的單元測試框架,即可解決好此類問題。
目前 Java 語言主流的單元測試框架有 JUnit 和 TestNG。Python 語言主流的單元測試框架有 unittest 。本小節(jié)的內容,主要介紹 TestNG 和 unittest 的使用,探討單元測試框架如何幫助自動化測試。
-
TestNG
接下來我們將會使用Java語言的
TestNG
框架展開“檢查”。TestNG為我們在項目測試中常用到的單元測試框架,很多程序員的理想套件,通過注解(annotation)的方式進行操作。在
TestNG
提供了@BeforeMethod
和@AfterMethod
,在每個測試函數(shù)調用之前/后都會調用。-
@BeforeMethod
: Method annotated with@BeforeMethod
executes before every test method. -
@AfterMethod
: Method annotated with@AfterMethod
executes after every test method.
如果在測試之前有些工作我們只想做一次,用不著每個函數(shù)之前都做一次,那就用下面兩個來標注:
-
@BeforeTest
: 在第一個 test method 開始執(zhí)行前,執(zhí)行。 -
@AfterTest
: 在最后一個 test method 執(zhí)行后再執(zhí)行。
接下來我們用具體的代碼示例,解釋單元測試框架的使用
-
TestNG 框架圖
TestNG單元測試框架示意圖.png -
TestNG 斷言
方法 Method 檢查條件 assertEquals(a, b [, msg])
a == b,msg可選,用來解釋失敗的原因 assertNotEquals(a, b [, msg]
a != b,msg可選,用來解釋失敗的原因 assertTrue(x [, msg])
x 是真,msg可選,用來解釋失敗的原因 assertFalse(x [, msg])
x 是假,msg可選,用來解釋失敗的原因 assertIsNot(a, b [, msg])
a 不是 b,msg可選,用來解釋失敗的原因 assertNull(x[, msg])
x 是null,msg可選,用來解釋失敗的原因 assertNotNull(x[, msg])
x 不是null,msg可選,用來解釋失敗的原因 -
TestNG 的引入
這里我們依舊使用 Maven 的方式,引入 TestNG 到項目中。
<dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.8</version> </dependency>
?
-
TestNG 的使用
/** * Created by Linty on 1/8/2017. * 使用 @BeforeTest 和 @AfterTest 進行測試框架操作 * 如果直接運行整個測試,運行步驟如下 * 首先運行 @BeforeTest * 然后運行 test01ChangeLanguage * 接著運行 test02LogIn * 最后運行 @AfterTest */ public class RanzhiMainTest { // 聲明兩個全局變量,每個方法都可以用下面的變量。 private WebDriver baseDriver = null; private String baseUrl = null; /** * 測試登錄 * 需要用 @Test 注解進行標注 * 這樣 不用main()方法 便可直接運行測試 * * @throws InterruptedException */ @Test public void test02LogIn() throws InterruptedException { WebDriver driver = this.baseDriver; driver.get(this.baseUrl); Thread.sleep(2000); driver.findElement(By.id("account")).sendKeys("admin"); driver.findElement(By.id("password")).sendKeys("123456"); driver.findElement(By.id("submit")).click(); // 點擊登錄按鈕后,需要等待瀏覽器刷新 Thread.sleep(2000); String expectedUrl = this.baseUrl + "sys/index.html"; // driver.getCurrentUrl() -- 獲取當前的瀏覽器URL Assert.assertEquals(driver.getCurrentUrl(), expectedUrl); } /** * 測試切換語言 * 把系統(tǒng)語言切換成 English * 然后查詢 語言的按鈕 是不是變成了 English * @throws InterruptedException */ @Test public void test01ChangeLanguage() throws InterruptedException { WebDriver driver = this.baseDriver; driver.get(this.baseUrl); Thread.sleep(2000); // 點擊語言按鈕 driver.findElement(By.cssSelector("#langs > button")).click(); Thread.sleep(500); // 用Css Selector 選擇 英文 driver.findElement(By.cssSelector("#langs > ul > li:nth-child(3) > a")).click(); // 瀏覽器需要刷新,等待2秒鐘 Thread.sleep(2000); // 檢查按鈕上的字是不是變成了 English String expected_language = "English"; String actual_language = driver.findElement(By.cssSelector("#langs > button")).getText(); Assert.assertEquals(actual_language, expected_language); } /** * 測試前置條件 * 在所有的測試開始前 執(zhí)行一次 */ @BeforeTest public void setUp() { this.baseDriver = new FirefoxDriver(); this.baseUrl = "http://demo.ranzhi.org/"; } /** * 測試清理操作 * 在所有的測試結束后 執(zhí)行一次 */ @AfterTest public void tearDown() { this.baseDriver.quit(); } }
?
-
-
unittest
接下來我們將會使用 Python 語言的
unittest
框架展開“檢查”。unittest
框架的原本的名字是PyUnit。是從JUnit 這樣一個被廣泛使用的 經典的Java應用開發(fā)的單元測試框架創(chuàng)造而來。類似的框架還有NUnit(.Net開發(fā)的單元測試框架)等。我們可以使用unittest框架為任意Python項目編寫可理解的單元測試集合。現(xiàn)在這個unittest已經作為Python的標準庫模塊發(fā)布。我們安裝完Python以后,便可以直接使用unittest。使用unittest需要以下簡單的三步:
- 引入unittest模組
- 繼承unittest.TestCase基類
- 測試方法以
test
開頭
unittest 并未使用 Java 語言常見的注解方式,依舊停留在 比較早期的 Java 版本中依靠方法名稱進行識別的方式。主要有以下兩個固定名字的方法:
- setUp():在每個測試方法運行前,執(zhí)行。是測試前置條件。
- tearDown():在每個測試方法運行后執(zhí)行,是測試清理操作。
具體的代碼如下:
class RanzhiMainTest(unittest.TestCase): """ 第一步:import unittest 第二步:繼承 unittest.TestCase 類 第三步:測試的方法,以test_ 開頭 第四步:重寫 setUp() 作為 測試前置條件,注意setUp的大小寫,必須一致 第五步:重寫 tearDown() 作為 測試清理操作,注意 tearDown的大小寫,必須一致 """ # 全局變量 base_driver = None base_url = None def setUp(self): self.base_driver = webdriver.Firefox() self.base_url = "http://demo.ranzhi.org/" def tearDown(self): self.base_driver.quit() def test_01_change_language(self): driver = self.base_driver driver.get(self.base_url) sleep(2) driver.find_element_by_css_selector("#langs > button").click() sleep(1) driver.find_element_by_css_selector("#langs > ul > li:nth-child(3) > a").click() sleep(2) # 頁面應該換成英語了 actual_lang = driver.find_element_by_css_selector("#langs > button").text expected_lang = "English" # 與Java的TestNG 相反,先寫期待值,再寫實際值 self.assertEqual(expected_lang, actual_lang) def test_02_log_in(self): driver = self.base_driver driver.get(self.base_url) sleep(2) driver.find_element_by_id("account").send_keys("admin") driver.find_element_by_id("password").send_keys("123456") driver.find_element_by_id("submit").click() sleep(3) actual_url = driver.current_url expected_url = self.base_url + "sys/index.html" self.assertEqual(expected_url, actual_url)
?
2.2 使用 Page Object 設計模式
Page Object設計模式是Selenium自動化測試項目的最佳設計模式之一,強調測試、邏輯、數(shù)據(jù)和驅動相互分離。
Page Object模式是Selenium中的一種測試設計模式,主要是將每一個頁面設計為一個Class,其中包含頁面中需要測試的元素(按鈕,輸入框,標題等),這樣在Selenium測試頁面中可以通過調用頁面類來獲取頁面元素,這樣巧妙的避免了當頁面元素id或者位置變化時,需要改測試頁面代碼的情況。當頁面元素id變化時,只需要更改測試頁Class中頁面的屬性即可。
它的好處如下:
- 集中管理元素對象,便于應對元素的變化
- 集中管理一個page內的公共方法,便于測試用例的編寫
- 后期維護方便,不需要重復的復制和修改代碼
具體的做法如下:
- 創(chuàng)建一個頁面的類
- 在類的構造方法中,傳遞 WebDriver 參數(shù)。
- 在測試用例的類中,實例化頁面的類,并且傳遞在測試用例中已經實例化的WebDriver對象。
- 在頁面的類中,編寫該頁面的所有操作的方法
- 在測試用例的類中,調用這些方法
實現(xiàn)的示例
-
Page 基類
設計了一個基本的Page類,以便所有的頁面進行繼承,該類標明了一個sub page類的基本功能和公共的功能。
-
全局變量: this.baseDriver,讓所有的子類都使用的。
// 基類的變量,所有繼承的類,都可以使用 BoxDriver baseDriver;
-
構造方法:
-
默認的構造方法,無參數(shù)的構造方法
public BasePage() { }
-
傳遞 driver的構造方法
public BasePage(BoxDriver driver) { this.baseDriver = driver; }
-
-
私有的常量:存放元素的定位符
private String START_BUTTON_SELECTOR = "s,#start > div"; private final String EXIT_MENU_TEXT = "l,%s";
-
成員方法:
-
每個子類都需要的系統(tǒng)功能:
-
open
public void open(String url) throws InterruptedException { this.baseDriver.navigate(url); Thread.sleep(2000); }
-
-
所有子類(頁面)都具有的業(yè)務功能
- selectApp
- logout
-
-
Sub Pages(s)子類
具體的頁面的類,定義了某個具體的頁面的功能
-
必須繼承基類
public class AdminPage extends BasePage { }
-
創(chuàng)建構造方法,帶driver 參數(shù)
public AdminPage(BoxDriver driver) { super(driver); }
特定頁面的業(yè)務
使用基類的
this.baseDriver
成員變量
-
Tests 類
這部分描述的是具體的測試用例。
-
聲明全局變量
private BoxDriver baseDriver = null; private String baseUrl = null; private LoginPage loginPage = null; private AdminPage adminPage = null;
-
調用各種頁面(pages)
-
實例化Page
this.loginPage = new LoginPage(this.baseDriver); this.adminPage = new AdminPage(this.baseDriver);
-
使用page的對象,調用成員方法
loginPage.open(this.baseUrl); loginPage.changeLanguage(lang); loginPage.login("admin", "123456", true); loginPage.selectApp(AppType.Admin); adminPage.clickAddMemberButton(); adminPage.addMemberData(member);
-
2.3 使用數(shù)據(jù)驅動
主要的數(shù)據(jù)驅動方式有兩種:
- 通過 文本文件或者 Excel 文件存儲數(shù)據(jù),并通過程序讀取數(shù)據(jù),遍歷所有的行
- 通過數(shù)據(jù)庫存儲數(shù)據(jù),并通過程序和 SQL 腳本讀取數(shù)據(jù),遍歷所有的行
通過 CSV 文件 或者 MySQL 數(shù)據(jù)庫,是主流的數(shù)據(jù)驅動方式。當然數(shù)據(jù)驅動也可以結合單元測試框架的參數(shù)化測試進行編寫(此部分本文不做具體描述)。
無論使用了 哪一種(CSV 或者 MySQL),讀取數(shù)據(jù)后都要進行遍歷操作。
-
Java 代碼
// 布爾型 true false boolean isFirstLine = true; // 循環(huán)每一個行,接下來根據(jù)每一行的值(數(shù)據(jù)),進行測試 for (CSVRecord row : csvData) { if (isFirstLine) { isFirstLine = false; continue; // continue的作用 // 當前循環(huán)到此為止,直接進入下一條循環(huán) } Member member = new Member(); member.setAccount(row.get(0)); member.setRealName(row.get(1)); if (Objects.equals(row.get(2), "f")) { member.setGender(Member.Gender.Female); } else { member.setGender(Member.Gender.Male); }
member.setDept(Integer.parseInt(row.get(3)));
member.setRole(Integer.parseInt(row.get(4)));
member.setPassword(row.get(5));
member.setEmail(row.get(6));
// TODO: 進行測試
}
?
- Python 代碼
```python
is_header = True
for row in csv_data:
if is_header:
is_header = False
continue
# dict 類型的數(shù)據(jù)
member_data = {
"account": row[0],
"real_name": row[1],
"gender": row[2],
"dept": row[3],
"role": row[4],
"password": row[5],
"email": row[6]
}
# TODO: 進行測試
?
2.4 封裝 Selenium WebDriver
封裝是一個面向對象編程的概念,是面向對象編程的核心屬性,通過將代碼內部實現(xiàn)進行密封和包裝,從而簡化編程。對Selenium進行封裝的好處主要有如下三個方面:
- 使用成本低
- 不需要要求所有的測試工程師會熟練使用Selenium,而只需要會使用封裝以后的代碼
- 不需要對所有的測試工程師進行完整培訓。也避免工作交接的成本。
- 測試人員使用統(tǒng)一的代碼庫
- 維護成本低
- 通過封裝,在代碼發(fā)生大范圍變化和遷移的時候,不需要維護所有代碼,只需要變更封裝的部分即可
- 維護代碼不需要有大量的工程師,只需要有核心的工程師進行封裝的維護即可
- 代碼安全性
- 對作為第三方的Selenium進行封裝,是代碼安全的基礎。
- 對于任何的代碼的安全隱患,必須由封裝來解決,使得風險可控。
- 使用者并不知道封裝內部的代碼結構。
封裝的具體示例:
-
找到一個指定輸入框(selector),并且輸入指定的字符(text)
type(selector, text)
不用在業(yè)務邏輯中,使用多次的
findElement(By.id(...))
public void type(String selector, String text) { WebElement we = this.locateElement(selector); we.clear(); we.sendKeys(text); }
-
找到一個可以點擊的元素(selector),并且點擊(click)
click(selector)
public void click(String selector) { this.locateElement(selector).click(); }
-
找到一個指定的frame,并且切換進去
switchToFrame(selector)
public void switchToFrame(String selector) { WebElement we = this.locateElement(selector); this.baseDriver.switchTo().frame(we); }
-
找到一個指定的select,并且通過index進行選擇
selectByIndex(selector, index)
public void selectByIndex(String selector, int index) { WebElement we = this.locateElement(selector); Select s = new Select(we); s.selectByIndex(index); }
以上的代碼是封裝了locateElement()
的幾種方法,在具體使用封裝過的代碼的時候,只需要簡單的調用即可。接下來的重點,是介紹 locateElement(selector)
的封裝方式。
- 查找元素:
findElement(By...)
- 支持各種的查找:8種方式都需要支持,必須通過
selector
顯示出分類-
selector
中需要包含一個特殊符號 - 實例化 封裝好的類的時候,需要約定好是什么特殊符號
- 強制性用
硬編碼 hard code
來實例化,例如,
或者?
或者 其他非常用字符=>
- 或者,構造方法中,傳遞
this.byChar
- 強制性用
-
- 要把查找到元素的返回給調用的地方:必須要有返回值,類型是
WebElement
private WebElement locateElement(String selector) {
WebElement we;
// 如果定位符中 有 分隔符,那么就從分隔符處分成兩段
// 第一段是By
// 第二段是真正的定位符
// 如果沒有分隔符,就默認用 id 定位
if (!selector.contains(this.byChar)) {
// 用 id 定位
we = this.baseDriver.findElement(By.id(selector));
} else {
// 用 分隔符 分成兩個部分
String by = selector.split(this.byChar)[0];
String value = selector.split(this.byChar)[1];
we = findElementByChar(by, value);
}
return we;
}
- 接下來的重點,是實現(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
類型 | 示例(分隔符以逗號, 為例) |
描述 |
---|---|---|
id | "account" 或者 "i,account" 或者 "id,account" | 分隔符左右兩側不可以空格 |
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 |
調用的具體示例
void logIn(String account, String password) throws InterruptedException {
BoxDriver driver = this.baseDriver;
driver.type("account", account);
driver.type("password", password);
driver.click("submit");
// 點擊登錄按鈕后,需要等待瀏覽器刷新
Thread.sleep(2000);
}
至此,自動化測試的方案如下圖所示:
- 封裝 Selenium 為 BoxDriver
- 在 測試用例中,實例化 BoxDriver,產生 bd 對象
- 使用 bd 對象,構造 業(yè)務模塊的實例化對象,產生 common
- 使用 common 在測試用例中,構建測試步驟
- 使用數(shù)據(jù)驅動的外部數(shù)據(jù),通過讀取,進行測試
- 執(zhí)行整個用例
2.5 使用 Git 進行源代碼管理
Git 是目前主流的源代碼管理工具,本文推薦的兩個 IDE 工具: JetBrains IDEA 和 JetBrains Pycharm 都是默認支持 Git 的。只需要按照以下步驟進行配置,便可以通過 IDE 工具對代碼進行提交,這樣可以防止代碼丟失,以及方便的查詢代碼的修改歷史,同時很方便團隊的編碼。
使用編程工具提交代碼(用IDEA 或者 PyCharm)
- 在本地的Git項目文件夾中創(chuàng)建項目
- 比如:git\selenium_pro
- 用IDEA 創(chuàng)建
Maven
項目- 注意
project location
務必在git\selenium_pro
- 比如 項目名字
HelloSelenium
- 項目路徑:
git\selenium_pro\HelloSelenium
- 注意
- 編寫 代碼
- 選擇 IDEA 的 VCS |
Enable Version Control Integration
- 彈出的窗口選擇 Git
- 所有的代碼文件名字變成紅色
- 右鍵 左側的項目名字
HelloSelenium
- 選擇
Git
|Add
- 所有的代碼文件名字變成綠色
- 選擇
- 右鍵 左側的項目名字
HelloSelenium
- 選擇
Git
|Commit Directory
- 左側填寫 說明
- 右側勾選
Reformat Code
- 選擇右下角
Commit And Push
- "XX文件已經committed"以后,點擊
Push
- 輸入用戶名 + 密碼,勾選 remember password
- push successful
- 選擇
3. 架構的構建
將第二部分的自動化測試方案付諸實踐,再對自動化測試的結果生成測試報告,便基本上實現(xiàn)了自動化架構的構建。比較重要的地方是第三方工具 Selenium WebDriver 的封裝工作。事實上,如果封裝了別的工具,便可以實現(xiàn)其他方面的自動化測試。
3.1 代碼的構建
在第二部分的基礎上,我們添加上去測試組織和測試報告的功能,并且將不同作用的代碼分開放入不同的文件夾中,實現(xiàn)代碼的構建。
-
測試用例的組織和執(zhí)行
- 測試集合 test suite
- 測試用例的集合
- 多個測試用例的類
- 測試用例類的方法
- 測試用例的集合
- 測試運行 運行 test suite
具體的方法
-
TestNG
在項目中,創(chuàng)建
testng.xml
文件如下<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="Default Suite"> <test name="SolutionSelenium"> <classes> <class name="cases.LoginTests"> <methods> <!-- 添加指定的測試 --> <include name="testLoginByCsv"/> <include name="testLoginByCsv2"/> <include name="testLoginByCsv3"/> </methods> </class> <!-- cases.LoginTests --> <class name="cases.AdminTests"> <methods> <!-- 去除指定的測試 --> <exclude name="testAddMember"/> </methods> </class> </classes> </test> <!-- SolutionSelenium --> </suite> <!-- Default Suite -->
- 指定測試的類
- 指定測試的類中的方法
- include: 一個個方法包含進來
- exclude: 去除指定的方法
然后,編寫測試入口的腳本:Main.java
public class Main { public static void main(String[] args) { TestNG test = new TestNG(); List<String> suites = new ArrayList<>(); suites.add("testng.xml"); test.setTestSuites(suites); test.run(); } }
?
- 測試集合 test suite
-
測試報告的生成
-
TestNG
測試報告
TestNG 自帶的測試報告 xml / html
-
ReportNG 測試報告的插件,停止開發(fā)和支持。
-
ExtentReport 測試報告
- TestNG有默認的測試報告生成器 Listener
- 使用 ExtentReport 重寫一個 Listener
- 讓 TestNG 使用我們寫好的 Listener 生成報告
- TestNG有默認的測試報告生成器 Listener
-
步驟
-
引入 ExtentReport 到 pom.xml
<dependency> <groupId>com.relevantcodes</groupId> <artifactId>extentreports</artifactId> <version>2.41.2</version> </dependency>
- 目前 ExtentReport 有兩個版本:2 和 3
- 2 全部開源的
- 3 有付費版和開源版
- 官方網址:http://extentreports.com/community/
- 目前 ExtentReport 有兩個版本:2 和 3
-
編寫 Listener : ExtentReporterNgListener
@Override public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) { Date date = new Date(); SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd_HHmmss"); String time = formatter.format(date); String reportName = String.format("ExtentReportTestNG_%s.html", time); // 創(chuàng)建報告 // NetworkMode.OFFLINE 支持斷網查看報告 this.extent = new ExtentReports( outputDirectory + File.separator + reportName, true, NetworkMode.OFFLINE); for (ISuite suite : suites) { Map<String, ISuiteResult> result = suite.getResults(); for (ISuiteResult r : result.values()) { ITestContext context = r.getTestContext(); // 創(chuàng)建測試節(jié)點 buildTestNodes(context.getPassedTests(), LogStatus.PASS); buildTestNodes(context.getFailedTests(), LogStatus.FAIL); buildTestNodes(context.getSkippedTests(), LogStatus.SKIP); } } extent.flush(); extent.close(); }
-
修改 testng.xml
在
<suite />
中添加<listener/>
<listeners> <listener class-name="runner.ExtentReporterNgListener"/> </listeners>
正常運行測試
-
-
-
unittest 的操作
-
添加 test suite
suite = unittest.TestSuite() suite.addTest(LoginTests("test_login_by_csv")) suite.addTest(LoginTests("test_login_by_csv2")) suite.addTest(AdminTests("test_add_member_by_csv"))
-
這里可以配置測試,到外部文件,數(shù)據(jù)庫中等。
測試類,測試方法 LoginTests,test_login_by_csv LoginTests,test_login_by_csv2 AdminTests,test_add_member_by_csv
?
-
-
實例化 test runner
# 測試報告的文件 test_time = time.strftime("%Y%m%d_%H%M%S", time.localtime()) report_file = open("reports\\ranzhi_automate_report_%s.html" % test_time, mode="wb") runner = HtmlTestRunner(stream=report_file, verbosity=2, title="然之系統(tǒng)自動化測試報告", description="具體測試報告內容如下: ")
HtmlTestRunner : 第三方測試報告運行器
-
用 test runner 去執(zhí)行測試,產生報告。
runner.run(suite)
?
-
3.2 使用持續(xù)集成
持續(xù)集成,Continuous integration ,簡稱CI。隨著軟件開發(fā)復雜度的不斷提高,團隊開發(fā)成員間如何更好地協(xié)同工作以確保軟件開發(fā)的質量已經慢慢成為開發(fā)過程中不可回避的問題。尤其是近些年來,敏捷(Agile) 在軟件工程領域越來越紅火,如何能再不斷變化的需求中快速適應和保證軟件的質量也顯得尤其的重要。
持續(xù)集成正是針對這一類問題的一種軟件開發(fā)實踐。首先我們看一下,敏捷教父 Martin Fowler
對持續(xù)集成的定義:
Martin Fowler:Continuous Integration is a software development practice where members of a team integrate their work frequently, usually each person integrates at least daily - leading to multiple integrations per day. Each integration is verified by an automated build (including test) to detect integration errors as quickly as possible. Many teams find that this approach leads to significantly reduced integration problems and allows a team to develop cohesive software more rapidly.
具體定義:持續(xù)集成式一種軟件開發(fā)實踐。它倡導團隊的成員必須經常的集成他們的工作,通常至少每天一次甚至更多次集成。每次集成都需要通過自動化的構建(包括編譯代碼、構建應用、部署程序以及自動化測試)來驗證,從而盡早盡快的發(fā)現(xiàn)集成中的錯誤。大量的團隊利用這樣的方式來更快的開發(fā)內聚的軟件。大大減少此過程中的集成問題。
持續(xù)集成強調開發(fā)人員提交了新代碼之后,立刻進行構建、(單元、自動化)測試。根據(jù)測試結果,我們可以確定新代碼和原有代碼能否正確地集成在一起。
首先,解釋下集成。我們所有項目的代碼都是托管在SVN服務器上。每個項目都要有若干個單元測試,并有一個所謂集成測試。所謂集成測試就是把所有的單元測試跑一遍以及其它一些能自動完成的測試。只有在本地電腦上通過了集成測試的代碼才能上傳到SVN服務器上,保證上傳的代碼沒有問題。所以,集成指集成測試。
再說持續(xù)。不言而喻,就是指長期的對項目代碼進行集成測試。既然是長期,那肯定是自動執(zhí)行的,否則,人工執(zhí)行則沒有保證,而且耗人力。對此,我們有一臺服務器,它會定期的從SVN中檢出代碼,并編譯,然后跑集成測試。每次集成測試結果都會記錄在案。完成這方面工作的就是下面要介紹的Jenkins軟件。當然,它的功能遠不止這些。在我們的項目中,執(zhí)行這個工作的周期是1天。也就是,服務器每1天都會準時地對SVN服務器上的最新代碼自動進行一次集成測試。
通過持續(xù)基礎,可以將自動化測試良好的應用起來,只要是代碼發(fā)生了變動,或者是進行代碼的構建,那么在構建后都可以通過持續(xù)基礎,自動的執(zhí)行測試腳本,驗證改進的功能。
當前主要的持續(xù)基礎工具有:
- Jenkins
- TeamCity
通過持續(xù)集成,可以進一步完善自動化測試的架構,使得自動化測試真正的幫助項目,保證測試質量。
3.3 參考代碼
本文的示例代碼,放到了Github上面,以下附上Github的地址。為了簡化操作,本代碼使用了 Selenium 2 的環(huán)境,搭配 46.0(含)以下的火狐瀏覽器,以便各位參考。
代碼將會持續(xù)更新。
代碼地址:https://github.com/lintyleo/seleniumpro
再次感謝各位花時間閱讀本文,由于時間緊迫,以及才疏學淺,文中難免出現(xiàn)各種疏漏,還請各位多多指教。不當之處,還歡迎各位指出,在此謝過。