本系列所有代碼?https://github.com/zhangting85/simpleWebtest
本文將介紹一個Java+TestNG+Maven+Selenium的web自動化測試腳本環境下selenium頁面對象設計下的頁面模塊的寫法,并提供全部代碼。
?
在一個頁面上,有的時候,會有一些需要重復利用的模塊。
比如,一個電子商務網站上,經常會在頁面最頂上有一個搜索框。這個搜索框在幾乎所有頁面上都會出現。可以隨時用它搜索一些商品。
這里,有人用繼承,寫一個父類,父類提供了這個搜索框的一些功能封裝。然后所有頁面類都繼承這個父類。
這樣寫一開始是沒問題的。但是當這類重用模塊增加了,變動了,會造成整個測試代碼邏輯結構亂成一團。所以不推薦。
?
這里介紹一下我的寫法:
把這些重復利用的部分作為頁面模塊。然后我對京東的首頁、搜索結果頁建立了頁面對象模型:
代碼如下:
?
首頁

1 /** 2 *京東首頁 3 */ 4 public class JDHomepage extends Page { 5 /** 6 *URL常量,很少用到,一般在起始頁用,有時放到配置文件里去統一管理 7 */ 8 private static final String URL="http://www.jd.com" ; 9 10 /** 11 *可供重用的頁面模塊,作為成員對象在顯示這個模塊的頁面中保存。 12 *這里用了組合的寫法(composite),注意不要濫用繼承。 13 */ 14 public SearchHeaderModule searchHeader= new SearchHeaderModule(); 15 16 /** 17 * 只有homepage之類的起始頁才必要有這個init方法用來打開URL。 18 * return this 表示執行完畢之后頁面仍舊在本頁。 19 * 如果留在本頁,并有頁面刷新,就要return new JDHomepage 20 * 如果沒有頁面刷新等頁面改變,就return this 21 * 如果跳轉到其他頁面,就return new xxxPage 22 * 這樣寫的好處,是每個方法的return語句上明確了頁面跳轉的預期結果 23 * Only the start page of a test case should has this init method 24 * @return return this means no page refresh and stay on this page after this method 25 * return new JDHomepage means stay on this page and has a page refresh 26 * return new xxxPage means page redirects after this method 27 */ 28 public JDHomepage init(){ 29 DriverManager.driver.get(URL); 30 return this ; 31 } 32 33 34 35 }
在首頁里我其實沒有封裝什么業務邏輯,正常來說如果實際去實現整個京東的測試用例,那么首頁這個類會變得比較龐大的。
這里我用下面這段代碼創建了SearchHeader這個頁面模塊
public SearchHeaderModule searchHeader= new SearchHeaderModule();
作為一個成員對象。這個對象的實例會在JDHomepage的構造方法被調用前先被jvm調用。
所以,每個Homepage的實例都會包含一個SearchHeader,然后我們只使用時如下調用即可:
home.init().searchHeader.search("巧克力");
home是一個JDHomepage類的實例,init方法是去打開這個page的URL,我只在首頁等起始頁上寫init方法。
search是searchHeader提供的方法,這樣直接連點調用即可。
?
SearchHeader的實現:

1 package simplewebtest.core.page.module.sample.jd; 2 3 import org.openqa.selenium.WebElement; 4 import org.openqa.selenium.support.FindBy; 5 6 import simplewebtest.core.Page; 7 import simplewebtest.core.page.sample.jd.JDItemlistPage; 8 9 10 /** 11 * 頁面模塊。此處表示京東各頁面上方共享的搜索條 12 * 他本身也可以看做是一個頁面 13 * 并以組合(composite)的形式嵌入外部網頁,注意不要濫用繼承 14 * this page module is composite to the outer page 15 */ 16 public class SearchHeaderModule extends Page { 17 18 /** 19 * PageFactory的寫法,用標簽來定義web elment的查找 define how to find a webelment by 20 * annotation 21 */ 22 @FindBy(id = "key" ) 23 WebElement searchInput; 24 25 @FindBy(xpath = "http://input[@value='搜索']" ) 26 WebElement searchButton; 27 28 /** 29 * 搜索一個關鍵字,先輸入文字,再按搜索按鈕 search a keyword 30 * 31 * @param keyword 32 * :搜索關鍵字 33 * @return 返回一個JDItemlistPage 34 */ 35 public JDItemlistPage search(String keyword) { 36 searchInput.sendKeys(keyword); 37 searchButton.click(); 38 return new JDItemlistPage(); 39 } 40 }
這個SearchHader就是一個普通的頁面對象。
注意所有的頁面對象里的封裝方法我都讓他返回類似 new JDItemlistPage()之類的頁面對象。
這樣我們在test case里可以連點。比如
home.init().searchHeader.search("巧克力").getProduct(1).getText();
至于連點造成調試困難?不,由于我們有事件監聽和自動log功能,調試不會很困難。
并且我通常是先寫線性代碼再重構成頁面對象,寫成這種的都是已經執行通過的代碼。
另外,我們不是每次都需要返回新的頁面對象實例,因為有時比做一個操作,頁面不會跳轉也不會變動。這時,return this;返回當前頁的實例就行了。
?
JDItemlistPage

1 package simplewebtest.core.page.sample.jd; 2 3 import java.util.List; 4 5 import org.openqa.selenium.By; 6 import org.openqa.selenium.WebElement; 7 import org.openqa.selenium.support.FindBy; 8 9 import simplewebtest.core.Page; 10 /** 11 *京東搜索商品結果頁 12 */ 13 public class JDItemlistPage extends Page { 14 15 16 /** 17 *先找所有商品的父親節點plist 18 */ 19 @FindBy(id = "plist" ) 20 public WebElement productList; 21 22 /** 23 *直接找第一個商品,XPATH表達式過長,無法閱讀。(你會看得頭疼嗎?我會。。。) 24 *注意這個xpath是由firepath自動生成的,冗余過度。如果你要用xpath,一定要會自己寫 25 *插件太傻,別依賴他。 26 */ 27 @FindBy(xpath = ".//*[@id='plist']/ul/li[1]/div/div[2]/a" ) 28 public WebElement firstproduct; 29 30 /** 31 *預先定位所有product 32 *get all products, suggested to use this way 33 */ 34 @FindBy(xpath = ".//*[@id='plist']//li" ) 35 public List<WebElement> products; 36 37 /** 38 *先找父親plist,讓父親來找兒子,這種寫法也是可以的,但是也不是特別好(這一定不是強迫癥) 39 *但是這個方法只能找第一個商品,想找第二個商品要再寫一個方法。不推薦。 40 */ 41 public String getFirstProductName() { 42 return productList.findElement(By.xpath("http://div[@class='p-name'][1]//a" )).getText(); 43 } 44 45 /** 46 *先找父親plist,讓父親來找兒子,但是加了一個傳入參數告訴父親要找第幾個兒子,也就是第幾個商品。(圣斗士嗎,這么多兒子) 47 *這樣我寫一次可以找到這個頁面上任意一個商品了,京東的網頁設計特別適合自動化,可能你要測的網站不是這么工整。 48 *這里的重點是:Xpath表達式是一個字符串,你可以隨意拼接。所以傳入參數number可以插進去。 49 *suggested 50 */ 51 public String getProductNameByIndexMethodOne( int number) { 52 return productList.findElement(By.xpath("http://div[@class='p-name']["+number+"]//a" )).getText(); 53 } 54 55 /** 56 *一次性找出所有product,然后取第幾個,我喜歡從1開始所以number-1,僅個人喜好。 57 *接著對找到的product執行getProductNameOf方法來獲取名字 58 *suggested 59 */ 60 public String getProductNameByIndexMethodTwo( int number) { 61 return getProductNameOf(products.get(number-1 )); 62 } 63 64 private String getProductNameOf(WebElement product) 65 { 66 return product.findElement(By.className("p-name" )).getText(); 67 68 } 69 70 71 }
這個頁面就是一個標準的頁面對象了
為了擴展一下,我增加了一些內容,比如尋找第一個商品的四種方法:
JDHomepage home = new JDHomepage(); // 結果頁面the expected result page JDItemlistPage resultPage=home.init().searchHeader.search("巧克力" ); // actual result: 用四種方法找出第一個商品名字,作為實際結果.(回字有五種寫法:P) String product_1 = resultPage.firstproduct.getText(); // 不推薦,但偶爾有適用場景 String product_2= resultPage.getFirstProductName(); // 不推薦,但偶爾有適用場景 String product_3= resultPage.getProductNameByIndexMethodOne(1); // 推薦寫法,但你方法名字不要這么長 String product_4= resultPage.getProductNameByIndexMethodTwo(1); // 推薦寫法,但你方法名字不要這么長
?
如上代碼中,(和JDItemlistPage的代碼結合起來看)
方法1直接用PageObject.WebElement來獲取商品,缺點是每個商品我都要定義一個WebElement
方法2先找到product list,再用一句寫死的Xpath來尋找第一個商品,缺點是個商品我都要寫一段寫死的Xpath
方法3先找到product list,再通過傳入參數來組合一段可用的Xpath,優點是我只要寫一次Xpath
方法4先找到所有product:
@FindBy(xpath = ".//*[@id='plist']//li" ) public List<WebElement> products;
然后再蔥存放WebElement的List里取第一個元素。我同樣要寫一次By.className定位。
?
對于尋找商品這樣的例子來說,推薦用方法3或4。對于一般的頁面元素推薦用方法1。對于一些其他特殊的場景,看情況使用方法2。
另外,JDItemListPage里也可以像首頁一樣加入一個SearchHeader的定義,這里沒加只是因為目前我用到的test case里沒有這個需要。
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
