不懂PO 設計模式?這篇實戰(zhàn)文帶你搞定 PO

為UI頁面寫測試用例時(比如web頁面,移動端頁面),測試用例會存在大量元素和操作細節(jié)。當UI變化時,測試用例也要跟著變化, PageObject 很好的解決了這個問題!
使用UI自動化測試工具時(包括selenium,appium等),如果無統(tǒng)一模式進行規(guī)范,隨著用例的增多會變得難以維護,而 PageObject 讓自動化腳本井井有序,將 page 單獨維護并封裝細節(jié),可以使 testcase 更穩(wěn)健,不需要大改大動。
具體做法:把元素信息和操作細節(jié)封裝到Page類中,在測試用例上調用Page對象(PageObject),比如存在一個功能“選取相冊標題”,需要為之建立函數(shù)selectAblumWithTitle(),函數(shù)內(nèi)部是操作細節(jié)findElementsWithClass(‘a(chǎn)lbum’)等:

以選“取相冊標題”舉例,偽代碼如下:
selectAblumWithTitle() {
?
?#選取相冊
??
?findElementsWithClass('album')?
?? ?#選取相冊標題
??
?findElementsWithClass('title-field')
??
?#返回標題內(nèi)容
? ?return getText()
}
page object的主要原則是提供一個簡單接口 (或者函數(shù),比如上述的selectAblumWithTitle),讓調用者在頁面上可以做任何操作,點擊頁面元素,在輸入框輸入內(nèi)容等。
因此,如果要訪問一個文本字段,page object應該有獲取和返回字符串的方法。page object應該封裝對數(shù)據(jù)的操作細節(jié),比如查找元素和點擊元素。當頁面元素改動時,應該只改變page類中的內(nèi)容,不需要改變調用它的地方。
不要為每個UI頁面都創(chuàng)建一個page類,應該只為頁面中重要的元素創(chuàng)建page類。
比如,一個頁面顯示多個相冊,應該創(chuàng)建一個相冊列表page object,它包含許多相冊page object。如果某些復雜UI的層次結構只是用來組織UI,那么它就不應該出現(xiàn)在page object中。page object的目的是通過給頁面建模,從而對應用程序的使用者變得有意義:

如果你想導航到另一個頁面,初始page對象應當return另一個page對象,比如點擊注冊,進入注冊頁面,在代碼中就應該return Register()。如果想獲取頁面信息,可以return基本類型(字符串、日期)。
建議不要在page object中放斷言。應該去測page object,而不是讓page object自己測自己,page object的責任是提供頁面的狀態(tài)信息。這里僅用HTML描述Page Object,這種模式還可以用來隱藏Java swing UI細節(jié),它可用于所有UI框架。
PageObject的核心思想是六大原則,掌握六大原則才可以進行 PageObject 實戰(zhàn)演練,這是 PageObject的精髓所在。 selenium官方凝聚出六大原則,后面的PageObject使用都將圍繞六大原則開展:
公共方法代表頁面提供的服務
不要暴露頁面細節(jié)
不要把斷言和操作細節(jié)混用
方法可以return到新打開的頁面
不要把整頁內(nèi)容都放到PO中
相同的行為會產(chǎn)生不同的結果,可以封裝不同結果
下面,對上述六大原則進行解釋:
原則一:要封裝頁面中的功能(或者服務),比如點擊頁面中的元素,可以進入到新的頁面,于是,可以為這個服務封裝方法“進入新頁面”。
原則二:封裝細節(jié),對外只提供方法名(或者接口)。
原則三:封裝的操作細節(jié)中不要使用斷言,把斷言放到單獨的模塊中,比如testcase。
原則四:點擊一個按鈕會開啟新的頁面,可以用return方法表示跳轉,比如return MainPage()表示跳轉到新的PO:MainPage。
原則五:只為頁面中重要的元素進行PO設計,舍棄不重要的內(nèi)容。
原則六:一個動作可能產(chǎn)生不同結果,比如點擊按鈕后,可能點擊成功,也可能點擊失敗,為兩種結果封裝兩個方法,click_success和click_error。
以企業(yè)微信首頁為例,企業(yè)微信首頁有二個主要功能:立即注冊和企業(yè)登錄。

點擊企業(yè)登錄可以進入登錄頁面,在頁面可以掃碼登錄和企業(yè)注冊。
點擊企業(yè)注冊可以進入注冊頁面,在頁面可以輸入相關信息進行注冊。
用page object原則為頁面建模,這里涉及三個頁面:首頁,登錄,注冊。在代碼中創(chuàng)建對應的三個類Index,Login,Register: ? 登陸頁?提供login findPassword功能 – Login類 + login findPassword?法 ? 登錄頁?內(nèi)的元素有多少并不關?,隱藏內(nèi)部界?控件 ? 登錄成功和失敗會分別返回不同的頁? – findPassword – loginSuccess – loginFail ? 通過?法返回值判斷登錄是否符合預期


BasePage是所有page object的父類,它為子類提供公共的方法,比如下面的BasePage提供初始化driver和退出driver,代碼中在base_page模塊的BasePage類中使用__init__初始方法進行初始化操作,包括driver的復用,driver的賦值,全局等待的設置(隱式等待)等等:
from time import sleep
from selenium import webdriver?
from selenium.webdriver.remote.webdriver
import WebDriver
class BasePage: ?
?def __init__(self, driver: WebDriver = None): ??
? ? ?#此處對driver進行復用,如果不存在driver,就構造一個新的 ??
? ? ?if driver is None: ? ??
? ? ?# Index頁面需要用,首次使用時構造新driver? ??
? ? ??self._driver = webdriver.Chrome() ?
? ? ? ? ?# 設置隱式等待時間 ??
? ? ? ? ?self._driver.implicitly_wait(3) ? ??
? ? ? ?# 訪問網(wǎng)頁 ? ? ??
? ? ?self._driver.get(self._base_url) ? ?
?? ?else: ? ? ? ?
? ?# Login與Register等頁面需要用這個方法,避免重復構造driver ? ? ? ? ? ?self._driver = driver ?
??def close(self): ?
? ? ?sleep(20) ? ?
?? ?self._driver.quit()
Index是企業(yè)微信首頁的page object,它存在兩個方法,進入注冊page object和進入登錄page object,這里return方法返回page object實現(xiàn)了頁面跳轉,比如:goto_register方法return Register,實現(xiàn)從首頁跳轉到注冊頁:
class Index(BasePage):
?
?_base_url = "https://work.weixin.qq.com/"?
? ?# 進入注冊頁面
?
?def goto_register(self):
??
? ? ?self._driver.find_element(By.LINK_TEXT, "立即注冊").click()
??
? ? ?# 創(chuàng)建Register實例后,可調用Register中的方法
?
? ? ?return Register(self._driver)
??
?# 進入登錄頁面
??
?def goto_login(self):
? ??
? ?self._driver.find_element(By.LINK_TEXT, "企業(yè)登錄").click()
??
? ? ?# 創(chuàng)建Login實例后,可調用Login中的方法
?
? ? ?return Login(self._driver)
Login是登錄頁面的page object,主要功能有:進入注冊頁面,掃描二維碼,因此創(chuàng)建兩個方法代表兩個功能:scan_qrcode和goto_registry。代碼跟上面相似,不過多介紹:
from selenium.webdriver.common.by import By?
from test_selenium.page.base_page import BasePage?
from test_selenium.page.register import Register?
class Login(BasePage):?
? ?# 掃描二維碼
??
?def scan_qrcode(self):
?
??pass
?
?# 進入注冊頁面
?
??def goto_registry(self):
??
??self._driver.find_element(By.LINK_TEXT, "企業(yè)注冊").click()
? ? ? ?return Register(self._driver)
Register是注冊頁面的page object,主要功能是填寫正確注冊信息,當填寫錯誤時,返回錯誤信息。register方法實現(xiàn)了正確的表格填寫,當填寫完畢時返回自身(頁面還停留在注冊頁)。get_error_message方法實現(xiàn)了錯誤填寫的情況,如果填寫錯誤,就收集錯誤內(nèi)容并返回:
from selenium.webdriver.common.by import By?
from test_selenium.page.base_page import BasePage?
class Register(BasePage):
?
?# 填寫注冊信息,此處只填寫了部分信息,并沒有填寫完全
??
?def register(self, corpname):
??
? ? ?# 進行表格填寫
? ?
?? ?self._driver.find_element(By.ID, "corp_name").send_keys(corpname)
? ? ? ?self._driver.find_element(By.ID, "submit_btn").click()
? ??
? ?# 填寫完畢,停留在注冊頁,可繼續(xù)調用Register內(nèi)的方法
? ??
? ?return self
? ?
#填寫錯誤時,返回錯誤信息
??
?def get_error_message(self):
? ?
?? ?# 收集錯誤信息并返回
?
? ? ?result=[]
? ?
?? ?for element in self._driver.find_elements(By.CSS_SELECTOR, ".js_error_msg"):
? ?
??result.append(element.text)
?
??return result
test_index模塊是對上述功能的測試,它獨立于page類,在TestIndex類中只需要調用page類提供的方法即可,比如下面對注冊頁及登陸頁的測試使用了test_register和test_login方法:
from test_selenium.page.index import Index
class TestIndex:
?
?# 所有步驟前的初始化
?
??def setup(self):? ? ??
? self.index = Index()
?
??# 對注冊功能的測試?
?? ?def test_register(self):
?
?? ? ?# 進入index,然后進入注冊頁填寫信息
? ? ? ?self.index.goto_register().register("霍格沃茲測試學院")?
?? ?# 對login功能的測試
??
?def test_login(self):
? ? ??
?# 從首頁進入到注冊頁
? ? ?
??register_page = self.index.goto_login().goto_registry()\
? ? ? ? ? ?.register("測吧(北京)科技有限公司")? ?
?? ? # 對填寫結果進行斷言,是否填寫成功或者填寫失敗
??
? ? ?assert "請選擇" in "|".join(register_page.get_error_message())?
?? ?# 關閉driver
??
?def teardown(self):
?
???self.index.close()