WebDriver 進階
歡迎閱讀WebDriver進階講義。本篇講義將會重點介紹Selenium WebDriver API的重點使用方法,以及使用模塊化和參數化進行自動化測試的設計。
WebDriver API 進階使用
元素定位
從之前的講義和學習中,我們知道,WebDriver API的調用以及自動化測試,務必從頁面元素的定位開始,那么回顧之前的內容,WebDriver提供了一系列的定位符以便使用元素定位方法。常見的定位符有以下幾種:
- id
- name
- class name
- tag
- link text
- partial link text
- xpath
- css selector
那么我們以下的操作將會基于上述的定位符進行定位操作。
對于元素的定位,WebDriver API可以通過定位簡單的元素和一組元素來操作。在這里,我們需要告訴Selenium如何去找元素,以至于他可以充分的模擬用戶行為,或者通過查看元素的屬性和狀態,以便我們執行一系列的檢查。
在Selenium2中,WebDriver提供了多種多樣的findElement(by.)
方法在一個網頁里面查找元素。這些方法通過提供過濾標準來定位元素。當然WebDriver也提供了同樣多種多樣的findElements(by.)
的方式去定位多個元素。
Selenium2提供的8個findElement(by.)
方法去定位元素。在這里我們來具體查看每個方法的詳細使用方式。下面的表格將會列出這些具體的方法:
方法Method | 描述Description | 參數Argument | 示例Example |
---|---|---|---|
id |
該方法通過ID的屬性值去定位查找單個元素 | id: 需要被查找的元素的ID | findElement(by.id("search")) |
name |
該方法通過name的屬性值去定位查找單個元素 | name: 需要被查找的元素的名稱 | findElement(by.name("q")) |
class name |
該方法通過class的名稱值去定位查找單個元素 | class_name: 需要被查找的元素的類名 | findElement(by.className("input-text")) |
tag_name |
該方法通過tag的名稱值去定位查找單個元素 | tag: 需要被查找的元素的標簽名稱 | findElement(by.tagName("input")) |
link_text |
該方法通過鏈接文字去定位查找單個元素 | link_text: 需要被查找的元素的鏈接文字 | findElement(by.linkText("Log In")) |
partial_link_text |
該方法通過部分鏈接文字去定位查找單個元素 | link_text: 需要被查找的元素的部分鏈接文字 | findElement(by.partialLinkText("Long")) |
xpath |
該方法通過XPath的值去定位查找單個元素 | xpath: 需要被查找的元素的xpath | findElement(by.xpath("http://*[@id='xx']/a")) |
css_selector |
該方法通過CSS選擇器去定位查找單個元素 | css_selector: 需要被查找的元素的ID | findElement(by.cssSelector("#search")) |
接下來的列表將會詳細展示find_elements_by
的方法集合。這些方法依據匹配的具體標準返回一系列的元素。
方法Method | 描述Description | 參數Argument | 示例Example |
---|---|---|---|
id |
該方法通過ID的屬性值去定位查找多個元素 | id: 需要被查找的元素的ID | findElements(by.id("search")) |
name |
該方法通過name的屬性值去定位查找多個元素 | name: 需要被查找的元素的名稱 | findElements(by.name("q")) |
class_name |
該方法通過class的名稱值去定位查找多個元素 | class_name: 需要被查找的元素的類名 | findElements(by.className("input-text")) |
tag_name |
該方法通過tag的名稱值去定位查找多個元素 | tag: 需要被查找的元素的標簽名稱 | findElements(by.tagName("input")) |
link_text |
該方法通過鏈接文字去定位查找多個元素 | link_text: 需要被查找的元素的鏈接文字 | findElements(by.linkText("Log In")) |
partial_link_text |
該方法通過部分鏈接文字去定位查找多個元素 | link_text: 需要被查找的元素的部分鏈接文字 | findElements(by.partialLinkText("Long")) |
xpath |
該方法通過XPath的值去定位查找多個元素 | xpath: 需要被查找的元素的xpath | findElements(by.xpath("http://div[contains(@class,'list')]")) |
css_selector |
該方法通過CSS選擇器去定位查找多個元素 | css_selector: 需要被查找的元素的ID | findElements(by.cssSelector(".input_class")) |
依據ID查找
請查看如下HTML的代碼,以便實現通過ID的屬性值去定義一個查找文本框的查找:
<input id="search" type="text" name="q" value=""
class="input-text" maxlength="128" autocomplete="off"/>
根據上述代碼,這里我們使用findElement(By.id())
的方法去查找搜索框并且檢查它的最大長度maxlength
屬性。我們通過傳遞ID的屬性值作為參數去查找,參考如下的代碼示例:
void testSearchTextFieldMaxLength(){
// get the search textbox
WebElement searchField = driver.findElement(By.id("search"))
// check maxlength attribute is set to 128
assertEqual("128", searchField.getAttribute("maxlength"))
}
如果使用findElement(By.id())
方法,將會返回所有的具有相同ID屬性值的一系列元素。
依據名稱name查找
這里還是根據上述ID查找的HTML代碼,使用findElement(By.id())
的方法進行查找。參考如下的代碼示例:
// get the search textbox
searchField = self.driver.findElement(By.name("q"))
同樣,如果使用findElements(By.name())
方法,將會返回所有的具有相同name屬性值的一系列元素。
依據class name查找
除了上述的ID和name的方式查找,我們還可以使用class name的方式進行查找和定位。
事實上,通過ID,name或者類名class name查找元素是最提倡推薦的和最快的方式。當然Selenium2 WebDriver也提供了一些其他的方式,在上述三類方式條件不足,查找無效的時候,可以通過這些其他方式來查找。這些方式將會在后續的內容中講述。
請查看如下的HTML代碼,通過改代碼進行練習和理解.
<button type="submit" title="Search" class="button">
<span><span>Search</span></span>
</button>
根據上述代碼,使用findElement(By.cssName())
方法去定位元素。
void testSearchButtonEnabled(){
// get Search button
searchButton = self.driver.findElement(By.cssName("button"))
// check Search button is enabled
assertTrue(searchButton.isEnabled())
}
同樣的如果使用findElements(By.cssName())
方法去定位元素,將會返回所有的具有相同name屬性值的一系列元素。
依據標簽名tag name查找
利用標簽的方法類似于利用類名等方法進行查找。我們可以輕松的查找出一系列的具有相同標簽名的元素。例如我們可以通過查找表中的<tr>
來獲取行數。
下面有一個HTML的示例,這里在無序列表中使用了<img>
標簽。
<ul class="promos">
<li>
<a >
<img src="/media/wysiwyg/homepage-three-column-promo-
01B.png" alt="Physical & Virtual Gift Cards">
</a>
</li>
<li>
<a >
<img src="/media/wysiwyg/homepage-three-column-promo-
02.png" alt="Shop Private Sales - Members Only">
</a>
</li>
<li>
<a href="http://demo.magentocommerce.com/accessories/
bags-luggage.html">
<img src="/media/wysiwyg/homepage-three-columnpromo-
03.png" alt="Travel Gear for Every Occasion">
</a>
</li>
</ul>
這里面我們使用findElements(By.tag())
的方式去獲取全部的圖片,在此之前,我們將會使用findElement(By.tag())
去獲取到指定的<ul>
。
具體代碼如下:
void testCountOfPromoBannersImages(){
// get promo banner list
bannerList = driver.findElement(By.className("promos"))
// get images from the banner_list
banners = bannerList.findElements(By.tagName("img"))
// check there are 20 tags displayed on the page
assertEqual(20, banners.length)
}
依據鏈接文字link Text查找
鏈接文字查找通常比較簡單。使用findElement(By.linkText())
請查看以下示例
<a href="#header-account" class="skip-link skip-account">
<span class="icon"></span>
<span class="label">Account</span>
</a>
測試代碼如下:
void testMyAccountLinkIsDisplayed(){
// get the Account link
accountLink =
driver.findElement(By.linkText("ACCOUNT"))
// check My Account link is displayed/visible in
// the Home page footer
self.assertTrue(accountLink.isDisplayed)
}
依據部分鏈接文字partial text查找
這里依舊使用上述的列子進行代碼編寫:
void test_account_links(){
// get the all the links with Account text in it
accountLinks = self.driver.findElements(By.partialLinkText("ACCOUNT"))
// check Account and My Account link is
displayed/visible in the Home page footer
assertTrue(2, accountLinks.length)
}
依據XPath進行查找
XPath是一種在XML文檔中搜索和定位節點node的一種查詢語言。所有的主流Web瀏覽器都支持XPath。Selenium2可以用強大的XPath在頁面中查找元素。
XPath 使用路徑表達式來選取 XML 文檔中的節點或者節點集。這些路徑表達式和我們在常規的電腦文件系統中看到的表達式非常相似。
常用的XPath的方法有starts-with()
,contains()
和ends-with()
等
若想要了解更多關于XPath的內容,請查看http://www.w3school.com.cn/xpath/index.asp
如下有一段HTML代碼,其中里面的<img>
沒有使用ID,name或者類屬性,所以我們無法使用之前的方法。亞這里我們可以通過<img>
的alt
屬性,定位到指定的tag。
<ul class="promos">
<li>
<a >
<img src="/media/wysiwyg/homepage-three-column-promo-
01B.png" alt="Physical & Virtual Gift Cards">
</a>
</li>
<li>
<a >
<img src="/media/wysiwyg/homepage-three-column-promo-
02.png" alt="Shop Private Sales - Members Only">
</a>
</li>
<li>
<a href="http://demo.magentocommerce.com/accessories/
bags-luggage.html">
<img src="/media/wysiwyg/homepage-three-columnpromo-
03.png" alt="Travel Gear for Every Occasion">
</a>
</li>
</ul>
具體代碼如下:
void testVipPromo(){
// get vip promo image
vipPromo = driver.findElement(By.xpath("http://img[@alt='Shop Private Sales - Members Only']"))
// check vip promo logo is displayed on home page
assertTrue(vipPromo.isDisplayed)
// click on vip promo images to open the page
vipPromo.click()
// check page title
assertEqual("VIP", driver.title)
}
當然,如果使用find_elements_by_xpath()
的方法,將會返回所有匹配了XPath查詢的元素。
依據CSS選擇器進行查找
CSS是一種設計師用來描繪HTML文檔的視覺的層疊樣式表。一般來說CSS用來定位多種多樣的風格,同時可以用來是同樣的標簽使用同樣的風格等。類似于XPath,Selenium2也可以使用CSS選擇器來定位元素。
請查看如下的HTML文檔。
<div class="minicart-wrapper">
<p class="block-subtitle">Recently added item(s)
<a class="close skip-link-close" href="#" title="Close">×</a>
</p>
<p class="empty">You have no items in your shopping cart.
</p>
</div>
我們來創建一個測試,驗證這些消息是否正確。
void testShoppingCartStatus(){
// check content of My Shopping Cart block on Home page
// get the Shopping cart icon and click to open the
// Shopping Cart section
shoppingCartIcon = driver.findElement(By.cssSelector("div.header-minicartspan.icon")
shoppingCartIcon.click()
// get the shopping cart status
shoppingCartStatus = driver.findElement(By.cssSelector("p.empty")).text
self.assertEqual("You have no items in your shopping cart.",
shoppingCartStatus)
// close the shopping cart section
closeButton = driver.findElement(By.cssSelector("div.minicart-wrappera.close")
closeButton.click()
}
鼠標事件
Web測試中,有關鼠標的操作,不只是單擊,有時候還要做右擊、雙擊、拖動等操作。這些操作包含在ActionChains類中。
常用的鼠標方法:
- contextClick() //右擊
- douchClick() //雙擊
- dragAndDrop() //拖拽
- moveToElement() //鼠標停在一個元素上
- clickAndHold() // 按下鼠標左鍵在一個元素上
鍵盤事件
鍵盤操作經常處理的如下:
代碼 | 描述 |
---|---|
sendKeys(Keys.BACK_SPACE) |
刪除鍵(BackSpace) |
sendKeys(Keys.SPACE) |
空格鍵(Space) |
sendKeys(Keys.TAB) |
制表鍵(Tab) |
sendKeys(Keys.ESCAPE) |
回退鍵(Esc) |
sendKeys(Keys.ENTER) |
回車鍵(Enter) |
sendKeys(Keys.CONTROL,'a') |
全選(Ctrl+A) |
sendKeys(Keys.CONTROL,'c') |
復制(Ctrl+C) |
特殊元素定位
iframe 操作
iframe 元素會創建包含另外一個文檔的內聯框架(即行內框架)。
在一個<html>中,包含了另一個<html>
示例
<html>
<head>
<title>iframe示例</title>
</head>
<body>
<h1>
這里是H1,標記了標題
</h1>
<p>
這里是段落,標記一個段落,屬于外層
</p>
<div>
<iframe id="iframe-1">
<html>
<body>
<p>
這里是個段落,屬于內層,內聯框架中的
</p>
<div id="div-1">
<p class="hahahp">
這里是div中的段落,需要被定位
</p>
</div>
</body>
</html>
</iframe>
</div>
</body>
</html>
需要定位上面示例中的<p>
:這里是div中的段落,需要被定位
如下是selenium WebDiriver的代碼
Java代碼
// 這里涉及了iframe操作
// 1. 找到 iframe 元素對應的 css selector,存到變量中
// 2. 調用driver.switchTo().frame(剛剛被找到的iframe元素)
WebElement frameElement = driver.findElement(By.cssSelector("#iframe-1"));
driver.switchTo().frame(frameElement);
// 進入紫禁城以后,正常操作
driver.findElement(By.cssSelector("XXX")).click();
Thread.sleep(2000);
// TODO:繼續填寫表單
// 在紫荊城結束以后必須退出來
// switchTo().frame()和 switchTo().defaultContent() 成對出現
driver.switchTo().defaultContent();
Python代碼
## 查找并定位 iframe
element_frame = driver.find_element_by_css_selector('#iframe-1')
## 切換到剛剛查找到的 iframe
driver.switch_to.frame(element_frame)
## 定位 <p>
driver.find_element_by_css_selector('#div-1 > p')
## TODO....
## 退出剛剛切換進去的 iframe
driver.switch_to.default_content()
Select 操作
<select>
是選擇列表Select 是個selenium的類
selenium.webdriver.support.select.Select
Select 類的路徑:
C:\Python35\Lib\site-packages\selenium\webdriver\support\select.py
<select id="brand">
<option value ="volvo">Volvo</option>
<option value ="saab">Saab</option>
<option value="opel">Opel</option>
<option value="audi">Audi</option>
</select>
示例,選擇 Audi
Java代碼
// 遇到 <select>
// 1. 定位 select,保存這個select 元素到變量中
// 2. 對變量進行操作,把變量作為一個參數,用new Select(變量)產生一個可操作的對象
// 3. 調用對象的方法
WebElement elementSelect = driver.findElement(By.cssSelector("#type"));
Select objectSelect = new Select(elementSelect);
objectSelect.selectByIndex(2);
Python代碼
## 查找并定位到 select
element_select = driver.find_element_by_css_selector('#brand')
## 用Select類的構造方法,實例化一個對象 object_select
object_select = Select(element_select)
## 操作 object_select
object_select.select_by_index(3)
## 也可以這樣
object_select.select_by_value('audi')
## 還可以這樣
object_select.select_by_visible_text('Audi')
高級用戶交互API
高級用戶交互API提供了一個更新更完善的機制來定義并描述用戶在一個網頁上的各種操作。這些操作包括:拖拽、按住CTRL鍵選擇多個元素等等。
快速上手
為了生成一連串的動作,我們使用Actions來建立。首先,我們先配置操作:
Actions builder = new Actions(driver);
builder.keyDown(Keys.CONTROL)
.click(someElement)
.click(someOtherElement)
.keyUp(Keys.CONTROL);
然后,獲得操作(Action):
Action selectMultiple = builder.build();
最后,執行這個動作:
selectMultiple.perform();
這一系列的動作應該盡量的短。在使用中最好在執行一個簡短的動作后驗證頁面是否處于正確的狀態,然后再執行下面的動作。下一節將會列出所有可用的動作(Action),并且說明它們如何進行擴展。
鍵盤交互(Keyboard interactions)
鍵盤交互是發生在一個特定的頁面元素的,而webdriver會確保這個頁面元素在執行鍵盤動作時處于正確的狀態。這個正確的狀態,包括頁面元素滾動到可視區域并定位到這個頁面元素。
既然這個新的API是面向用戶(user-oriental)的接口,那么對于一個用戶,在對一個元素輸入文本前做顯式的交互就更加的符合邏輯。這意味著,當想定位到相鄰的頁面元素時,可能需要點擊一下元素或按下Tab(Keys.TAB
)鍵。
The new interactions API will (first) support keyboard actions without a provided element. The additional work to focus on an element before sending it keyboard events will be added later on.
鼠標交互(Mouse interactions)
鼠標操作有一個上下文-鼠標的當前位置。因此,當為幾個鼠標操作設定一個上下文時,第一個操作的上下文就是元素的相對位置,下一個操作的上下文就上一個操作后的鼠標相對位置。
單個動作
所有的動作都實現了Action
接口,這個接口只有一個方法:perform()
。每個動作所需要的信息都通過Constructor傳入。當調用這個動作的時候,動作知道如何與頁面交互(如,找到活動的元素并輸入文本或者計算出在屏幕上的點擊坐標)并且調用底層實現來實現這個交互。
下面是一些動作:
- ButtonReleaseAction - 釋放鼠標左鍵
- ClickAction - 相當于 WebElement.click()
- ClickAndHoldAction - 按下鼠標左鍵并保持
- ContextClickAction - 一般就是鼠標右鍵,通常調出右鍵菜單用。
- DoubleClickAction - 雙擊某個元素
- KeyDownAction - 按下修飾鍵(SHIFT,CTRL,ALT,WIN)
- KeyUpAction - 釋放修飾鍵
- MoveMouseAction - 移動鼠標從當前位置到另外的元素.
- MoveToOffsetAction - 移動鼠標到一個元素的偏移位置(偏移可以為負,元素是鼠標剛移動到的那個元素)。
- SendKeysAction - 相當于 WebElement.sendKey(...)
CompositeAction
包含一系列的動作,當被調用的時候,它會調用它所包含的所有動作的perform方法。通常,這個動作通常都不是直接建立的,一般是使用ActionChainsGenerator
。
生成動作鏈
Actions
鏈生成器實現了創建者模式來新建一個包含一組動作的CompositeAction
。使用Actions生成器可以很容易的生成動作并調用build()
方法來獲得復雜的操作。
Actions builder = new Actions(driver);
Action dragAndDrop = builder.clickAndHold(someElement)
.moveToElement(otherElement)
.release(otherElement)
.build();
dragAndDrop.perform();
有一個對Actions
進行擴展的計劃,給Actions
類添加一個方法,這個方法可以追加任何動作到其擁有的動作列表上。這將允許添加擴展的動作,而不用人工創建CompositeAction。關于擴展Actions
,請往下看。
擴展Action接口的指導
Action接口只有一個方法-perform()
。除了實際的交互本身,所有的條件判斷也都應該在這個這個方法里實現。在動作創建和動作實際執行這段時間內,很可能頁面的狀態已經發生了變化,比如元素的可視情況已經坐標已經不能找到了。
實現細節
為了達到每個操作的執行與具體實現的分離,所有的動作都依賴2個接口:Mouse
和Keyboard
。這些接口被所有支持高級用戶接口API的driver實現了。需要注意的是,這些接口是為了讓動作使用的,而不是最終用戶。本節的信息,主要是針對想擴展WebDriver的開發者的。
一字警告
Keyboard
和Mouse
接口是設計用來支持各種Action類的。有鑒于此,它們的API沒有Actions鏈生成器API穩定。直接使用這些接口可能達不到期望的結果,因為Action類做了額外的工作來確保在實際事件觸發時處于正確的環境條件。這些準備工作包括定位在正確的元素上或者鼠標交互前保證元素是可見的。
Keyboard接口
Keyboard接口包含3個方法:
- void sendKeys(CharSequence... keysToSend) - 與 sendKeys(...)相似.
- void pressKey(Keys keyToPress) - 按下一個鍵并保持。鍵僅限于修飾鍵(Control, Alt and Shift).
- void releaseKey(Keys keyToRelease) - 釋放修飾鍵.
至于如何在調用之間保存修飾鍵的狀態是Keyboard接口實現類的職責。只有活躍的元素才會接收到這些事件。
Mouse接口
Mouse
接口包含以下方法(有可能不久之后會有變化):
- void click(WebElement onElement) - 同click()方法一樣.
- void doubleClick(WebElement onElement) - 雙擊一個元素.
- void mouseDown(WebElement onElement) - 在一個元素上按下左鍵并保持 Action selectMultiple = builder.build();
- void mouseUp(WebElement onElement) - 在一個元素上釋放左鍵.
- void mouseMove(WebElement toElement) - 從當前位置移動到一個元素
- void mouseMove(WebElement toElement, long xOffset, long yOffset) - 從當前位置移動到一個元素的偏移坐標
- void contextClick(WebElement onElement) - 在一個元素上做一個右鍵操作
Native events(原生事件) VS synthetic events(合成事件)
WebDriver提供的高級用戶接口,要么是直接模擬的Javascript事件(即合成事件),要么就是讓瀏覽器生成Javascript事件(即原生事件)。原生事件能更好的模擬用戶交互,而合成事件則是平臺獨立的,這使得使用了替代的窗口管理器的linux系統顯得尤其重要,具體參加native events on Linux。原生事件無論什么時候總是應該盡可能的使用。
下面的表格展示了瀏覽器對事件的支持情況。
瀏覽器 | 操作系統 | 原生事件 | 合成事件 |
---|---|---|---|
Firefox | Linux | 支持 | 支持(默認) |
Firefox | Windows | 支持(默認) | 支持 |
Internet Explorer | Windows | 支持(默認) | 不支持 |
Chrome | Linux/Windows | 支持* | 不支持 |
Opera | Linux/Windows | 支持(默認) | 不支持 |
HtmlUnit | Linux/Windows | 支持(默認) | 不支持 |
ChromeDriver提供了2種模式來支持原生事件:Webkit事件和原始事件。其中Webkit事件是使用Webkit函數來觸發的Javascript事件,而原始事件模式則使用的是操作系統級別的事件。
FirefoxDriver中,原生事件可以使用FirefoxProfile來進行開關控制。
FirefoxProfile profile = new FirefoxProfile();
profile.setEnableNativeEvents(true);
FirefoxDriver driver = new FirefoxDriver(profile);
例子
以下是原生事件與合成事件表現不同的一些例子:
- 使用合成事件,點擊隱藏在其他元素下面的元素是可以的。使用原生事件,瀏覽器會將點擊事件作用在所給坐標最外層的元素上,就像是用戶點擊在特定的位置一樣。
- 當一個用戶,按下TAB鍵希望焦點從當前元素定位到下一個元素,瀏覽器是可以做到的。使用合成事件的話,瀏覽器并不知道TAB鍵被按下了,因此也不會改變焦點。而使用原生事件,瀏覽器則會表現正確。
模塊化與類庫
線性測試
至此之前,我們介紹的測試腳本,盡管使用了unittest測試框架,但是測試是按照指定的線路進行的,是線性的測試,完全遵循了腳本的執行順序。
測試腳本1
driver = FirefoxDriver()
driver.get("http://wwww.xxx.com")
driver.findElement(By.id("tbUserName")).sendKeys("username")
driver.findElement(By.id(("tbPassword")).sendKeys("123456")
driver.findElement(By.id(("btnLogin")).click()
#執行具體用例操作
......
driver.quit ()
如上圖,其實登錄的模塊可以共用。
模塊化
- 使用模塊化的業務組裝測試
- 業務從測試用例抽離
- 具體步驟:
用IDEA新建maven項目
修改pom.xml未指定的內容
右上角(或者右下角)點擊
Enable Auto-import
在
src/main/java
新建Java Class(測試用例)-
新建的測試用例中寫三個方法:(需要testNg的注解)
-
public void setUp
(@BeforeTest) -
public void tearDown
(@AfterTest) -
public void testXxx
(@Test)
-
在
src/main/java
新建Java Class2(測試業務)-
在新建的測試業務類中寫業務的方法:
- openUrl
- changeLanguage
- logIn
-
在新建的測試業務類中寫
構造方法
注意需要傳遞
driver
和url
public RanzhiCommon(WebDriver driver, String url){ this.baseDriver = driver; this.baseUrl = url; }
模塊話是自動化測試的第一個延伸和基礎。需要對自動化重復編寫的腳本進行重構(refactor),將重復的腳本抽取出來,放到指定的代碼文件中,作為共用的功能模塊。
測試腳本1:RanzhiCommon.java
/**
* 實際上的登錄方法
*
* @param account:用戶名
* @param password:密碼
*/
public void logIn(String account, String password) {
WebDriver driver = this.driver;
// 輸入用戶名
WebElement accountElement = driver.findElement(By.id("account"));
accountElement.clear();
accountElement.sendKeys(account);
// 輸入密碼
WebElement passwordElement = driver.findElement(By.id("password"));
passwordElement.clear();
passwordElement.sendKeys(password);
// 點擊登錄按鈕
driver.findElement(By.id("submit")).click();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
另一份文件 RanzhiCommon.java
/**
* 登出系統
*/
public void logOut() {
WebDriver driver = this.driver;
driver.findElement(By.id("start")).click();
driver.findElement(By.linkText("退出")).click();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
自動化的測試:代碼如下
package com.hello;
import com.hello.library.RanzhiCommon;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
/**
* Created by Linty on 9/11/2016.
* 使用模塊化的測試用例
*/
public class RanzhiTestCase02 {
// 成員變量
private WebDriver driver;
private String baseUrl;
private RanzhiCommon common;
@Before
public void setUp() {
this.driver = new FirefoxDriver();
this.baseUrl = "http://172.31.95.220/ranzhi/www";
this.common = new RanzhiCommon(this.driver, this.baseUrl);
}
@After
public void tearDown() {
this.driver.quit();
this.common = null;
}
@Test
public void testLoginWithModule() {
WebDriver driver = this.driver;
RanzhiCommon common = this.common;
// 步驟一:打開頁面
common.openWebPage("/");
Assert.assertEquals("登錄頁面打開錯誤",
this.baseUrl + "/sys/index.php?m=user&f=login&referer=L3JhbnpoaS93d3cvc3lzL2luZGV4LnBocA==",
driver.getCurrentUrl());
// 步驟二:切換語言
String actualLanguage = common.changeChinese();
Assert.assertEquals("系統語言切換失敗", "簡體", actualLanguage);
// 步驟三:進行登錄
common.logIn("admin", "123456");
Assert.assertEquals("登錄頁面登錄跳轉失敗",
this.baseUrl + "/sys/index.php?m=index&f=index",
driver.getCurrentUrl());
// 步驟四:退出系統
common.logOut();
Assert.assertEquals("登錄頁面退出跳轉失敗",
this.baseUrl + "/sys/index.php?m=user&f=login",
driver.getCurrentUrl());
}
}
參數化驅動
數據驅動
如果說模塊化是自動化測試的第一步,那么數據驅動是自動化的第二步,從本意上來講。數據改變更新驅動自動化的執行。從而引起測試結果的改變。其實類似于參數化。
-
使用數據驅動測試
- 測試數據從用例抽離
- 常見的測試數據的形式:
- 外部文件(文本文件、Excel(帶有格式,不容易讀))
- csv(默認是用"
,
"隔開每一列) - txt
- csv(默認是用"
- 數據庫的方式
- MySQL (輕便)
- Oracle/ SQL Server
-
具體進行數據驅動的方式:
- 找一個類,或者編寫一個類
- 找讀取csv的類:maven的方式
- 找一個類,或者編寫一個類
示例代碼
@Test
public void testAddBatchUserByCsv() {
// 讀取 csv 文件到FilerReader中
// 用捕獲異常的方式 進行文件讀取,防止出現“文件不存在”的異常
Reader in = null;
try {
in = new FileReader("src/main/resources/team_member.csv");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 讀取 csv 到 records中
Iterable<CSVRecord> records = null;
try {
records = CSVFormat.EXCEL.parse(in);
} catch (IOException e) {
e.printStackTrace();
}
// 遍歷 records,循環添加 userToAdd
for (CSVRecord record : records) {
RanzhiUser userToAdd = new RanzhiUser(
record.get(0), record.get(1),
Integer.parseInt(record.get(3)),
Integer.parseInt(record.get(4)),
record.get(2).toCharArray()[0],
record.get(5),
record.get(0) + record.get(6)
);
baseRanzhiCommon.login("admin", "123456");
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedMainUrl = baseUrl + "sys/index.php?m=index&f=index";
Assert.assertEquals("登錄成功主頁跳轉失敗", expectedMainUrl, baseDriver.getCurrentUrl());
baseRanzhiCommon.selectApp(RanzhiApp.Admin);
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedAdminUrl = baseUrl + "sys/index.php?m=admin&f=index";
Assert.assertEquals("后臺管理主頁跳轉失敗", expectedAdminUrl, baseDriver.getCurrentUrl());
baseDriver.switchTo().frame("iframe-superadmin");
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
baseRanzhiCommon.selectSubMenuForAdmin(AdminSubMenu.Organization);
String expectedOrganizationUrl = baseUrl + "sys/index.php?m=admin&f=index";
Assert.assertEquals("后臺管理組織跳轉失敗", expectedOrganizationUrl, baseDriver.getCurrentUrl());
baseRanzhiCommon.clickAddUserButton();
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedAddUserUrl = baseUrl + "sys/index.php?m=user&f=create";
Assert.assertEquals("添加成員主頁跳轉失敗", expectedAddUserUrl, baseDriver.getCurrentUrl());
baseRanzhiCommon.addNewUser(userToAdd);
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
try {
sleep(5000);
} catch (InterruptedException ignored) {
}
String expectedUserSavedUrl = baseUrl + "sys/index.php?m=user&f=admin";
Assert.assertEquals("用戶保存跳轉失敗", expectedUserSavedUrl, baseDriver.getCurrentUrl());
baseDriver.switchTo().defaultContent();
baseRanzhiCommon.logout();
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedLogoutUrl = baseUrl + "sys/index.php?m=user&f=login";
Assert.assertEquals("退出登錄頁面跳轉錯誤", expectedLogoutUrl, baseDriver.getCurrentUrl());
baseRanzhiCommon.login(userToAdd.getAccount(), userToAdd.getPassword());
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedMainUrl2 = baseUrl + "sys/index.php?m=index&f=index";
Assert.assertEquals("登錄成功主頁跳轉失敗", expectedMainUrl2, baseDriver.getCurrentUrl());
baseRanzhiCommon.logout();
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedLogoutUrl2 = baseUrl + "sys/index.php?m=user&f=login";
Assert.assertEquals("退出登錄頁面跳轉錯誤", expectedLogoutUrl2, baseDriver.getCurrentUrl());
}
}
關于參數化驅動,我們可以將數據放到csv中,然后通過讀取csv的數據進行自動化測試。同時也可以使用數據庫嘗試,代碼如下:
@Test
public void testAddBatchUserByDb() {
Statement stmt = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
Connection conn = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost/test?" +
"user=root&password=");
} catch (SQLException e) {
e.printStackTrace();
}
stmt = conn.createStatement();
String sql = "SELECT \n" +
" `account`,\n" +
" `realname`,\n" +
" `gender`,\n" +
" `dept`,\n" +
" `role`,\n" +
" `password`,\n" +
" `email` \n" +
"FROM\n" +
" `test`.`userlist` \n" +
"LIMIT 0, 1000 ;";
rs = stmt.executeQuery(sql);
// or alternatively, if you don't know ahead of time that
// the query will be a SELECT...
if (stmt.execute(sql)) {
rs = stmt.getResultSet();
System.out.println(rs.next());
}
if (rs != null) {
while (rs.next()) {
RanzhiUser userToAdd = new RanzhiUser(
rs.getString("account"),
rs.getString("realname"),
Integer.parseInt(rs.getString("role")),
Integer.parseInt(rs.getString("dept")),
rs.getString("gender").toCharArray()[0],
rs.getString("password"),
rs.getString("account") + rs.getString("email")
);
baseRanzhiCommon.login("admin", "123456");
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedMainUrl = baseUrl + "sys/index.php?m=index&f=index";
Assert.assertEquals("登錄成功主頁跳轉失敗", expectedMainUrl, baseDriver.getCurrentUrl());
baseRanzhiCommon.selectApp(RanzhiApp.Admin);
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedAdminUrl = baseUrl + "sys/index.php?m=admin&f=index";
Assert.assertEquals("后臺管理主頁跳轉失敗", expectedAdminUrl, baseDriver.getCurrentUrl());
baseDriver.switchTo().frame("iframe-superadmin");
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
baseRanzhiCommon.selectSubMenuForAdmin(AdminSubMenu.Organization);
String expectedOrganizationUrl = baseUrl + "sys/index.php?m=admin&f=index";
Assert.assertEquals("后臺管理組織跳轉失敗", expectedOrganizationUrl, baseDriver.getCurrentUrl());
baseRanzhiCommon.clickAddUserButton();
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedAddUserUrl = baseUrl + "sys/index.php?m=user&f=create";
Assert.assertEquals("添加成員主頁跳轉失敗", expectedAddUserUrl, baseDriver.getCurrentUrl());
baseRanzhiCommon.addNewUser(userToAdd);
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
try {
sleep(5000);
} catch (InterruptedException ignored) {
}
String expectedUserSavedUrl = baseUrl + "sys/index.php?m=user&f=admin";
Assert.assertEquals("用戶保存跳轉失敗", expectedUserSavedUrl, baseDriver.getCurrentUrl());
baseDriver.switchTo().defaultContent();
baseRanzhiCommon.logout();
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedLogoutUrl = baseUrl + "sys/index.php?m=user&f=login";
Assert.assertEquals("退出登錄頁面跳轉錯誤", expectedLogoutUrl, baseDriver.getCurrentUrl());
baseRanzhiCommon.login(userToAdd.getAccount(), userToAdd.getPassword());
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedMainUrl2 = baseUrl + "sys/index.php?m=index&f=index";
Assert.assertEquals("登錄成功主頁跳轉失敗", expectedMainUrl2, baseDriver.getCurrentUrl());
baseRanzhiCommon.logout();
baseDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
String expectedLogoutUrl2 = baseUrl + "sys/index.php?m=user&f=login";
Assert.assertEquals("退出登錄頁面跳轉錯誤", expectedLogoutUrl2, baseDriver.getCurrentUrl());
}
}
// Now do something with the ResultSet ....
} catch (SQLException ex) {
// handle any errors
System.out.println("SQLException: " + ex.getMessage());
System.out.println("SQLState: " + ex.getSQLState());
System.out.println("VendorError: " + ex.getErrorCode());
} finally {
// it is a good idea to release
// resources in a finally{} block
// in reverse-order of their creation
// if they are no-longer needed
if (rs != null) {
try {
rs.close();
} catch (SQLException sqlEx) {
} // ignore
rs = null;
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException sqlEx) {
} // ignore
stmt = null;
}
}
}
Git的使用
- git
git vs svn
配置管理工具
手工測試:文檔(測試計劃、用例、測試報告、需求、用戶手冊)
開發:源代碼管理
-
安裝Git的步驟
教室內網安裝包:
\\172.31.95.220\share\01-新教學軟件\01-自動化測試\git安裝
- 安裝git程序
git官網:https://git-scm.com
git本身是命令行工具
官網推薦git桌面工具:SourceTree - 安裝TortoiseGit客戶端
- 安裝git程序
-
選擇Git倉庫
- 最有名的 https://github.com,開源代碼的主戰場
- 國內的
- coding.net
- 碼云:git.oschina.net
- code.csdn.net
- 選擇 coding.net
-
具體使用步驟:
- 在coding.net 初始化Git倉庫(先注冊用戶,登錄)
- 創建項目(啟用README.md初始化項目)
- md文件:Markdown文件
- 在coding.net的項目左側,找到
代碼
,復制旁邊的路徑 - 在非系統盤創建文件夾 git
- 進入git,右鍵選擇
git clone
- 彈出的對話框中,默認就有剛剛復制的路徑,檢查文件存放的位置
- 比如 git\selenium_weekend
- 確定,輸入用戶名 + 密碼
- 初始化成功
-
修改文件并提交(用TortoiseGit操作)
- 從Git倉庫更新當前的文件
- TortoiseGit右鍵,選擇Git Sync
- 彈出的對話框中,選擇pull:拉取文件
- 如果是私有Git倉庫,需要輸入用戶名 + 密碼
- 編輯需要的文件
- 提交所處理的變動
- TortoiseGit右鍵,選擇Git Commit
- 選擇push:提交文件
- 如果是私有Git倉庫,需要輸入用戶名 + 密碼
- 從Git倉庫更新當前的文件
-
使用編程工具提交代碼(用IDEA或者PyCharm)
- 在本地的Git項目文件夾中創建項目
- 比如:git\selenium_weekend
- 用IDEA 創建
Maven
項目- 注意
project location
務必在git\selenium_weekend
- 比如 項目名字
HelloSelenium
- 項目路徑:
git\selenium_weekend\HelloSelenium
- 注意
- 覆蓋
pom.xml
- 編寫 代碼
- 選擇 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
- 選擇
- 在本地的Git項目文件夾中創建項目
階段小結
這里的數據換成了特別的數據,就是關鍵字。
Selenium IDE 的作用
通過錄制頁面元素的定位和操作,進行自定義命令的編輯,(Select定位頁面元素),導出指定的帶單元測試框架的腳本(自動化測試用例),輔助代碼編寫,以及快速入門。-
Selenium Java 環境搭建
使用maven的方式,管理依賴項,進行環境搭建 新建maven的項目,編輯項目中的 pom.xml,*.jar jar包
- 依賴項要添加對,包名和版本
- 電腦聯網,需要連接訪問maven倉庫
-
Selenium WebDriver 的元素定位
需要注意iFrame-
單一元素定位
id, name, class name(不靠譜), tag name(不靠譜)
xpath, css selector, link text, partial link textclass是以
.
開頭id 是以
#
開頭css selector:
#langs > button
xpath:
//*[@id="langs"]/button
-
xpath(絕對路徑):
/html/body/div/div/div[1]/div/div/button
解釋
<html> <body> <div> <div> <div> <div> <div> <button></button> </div> </div> </div> <div></div> </div> <div> </body> </html>
?
-
定位一系列元素
id name (不靠譜)
class name
css selectorxpath
-
-
Selenium 使用 單元測試框架
Java: junit4/TestNG Python: unittest
項目 Java Python 方法名 小駱駝 testRanzhiLogin() 類C的命名test_ranzhi_login() 用例的步驟 @Test(方法以test開頭) test_開頭 用例的前置條件 @Before(方法 setUp) 重寫setUp() 用例的清場方式 @After(方法 tearDown) 重寫tearDown() 依賴的處理 添加junit依賴項到pom.xml import unittest 寫一個測試用例類,繼承unittest.TestCase -
Selenium 使用 模塊化 用例設計
library文件夾
RanzhiCommon.java-
面向對象:實例化一個對象(注意構造方法),調用對象的成員,包括普通方法,變量
Java實體類
Java枚舉類型設計模式:自動化測試框架,頁面對象模式
python 面向對象
dict類型
-
Selenium 使用 數據驅動 進行用例設計和執行
- 讀文件進行測試(準備的用例數據)
- 讀數據庫進行測試(不推薦)
- 代碼中,注意關閉數據庫連接
- 代碼中,注意SQL腳本的效率,
- 替代方案,導出數據庫的數據到文件中,然后讀文件,或者把該文件導入本地的數據庫,注意類型轉換,尤其是日期類型。
- 新建一個
CsvUtility.java
,新建一個DbUtility.java
-
Selenium 工具的使用匯總 IDEA 和 Git
`IDEA` `PyCharm` 打開對的文件夾(作為項目) Reformat Code (Ctrl + Alt + L) Refactor | rename 重構,修改名字,批量關聯的級聯修改 按下ctrl+鼠標左鍵,導航定位的聲明處 紅燈處理,選中錯誤的行,然后等紅燈出來,點右上角的箭頭 Java里面 用 Alt + Enter 鍵 Git的集成: github.io coding.net git.oschina.net bitbucket.org code.csdn.net(騰訊的測試工具 apt)
- 在線建倉庫
- git clone 下載到本地(需要新建非系統盤的 git文件夾)
- git pull 獲取遠程變化到本地
- commit 提交到本地
- push 提交到遠程
集成在IDEA中
菜單 VCS | Enable ……,里面選擇git
提交git 需要先add
導出一份 完全沒有版本管理信息的代碼。 export
- 在線建倉庫