PO模型
前言
PO模型是:Page Object Model的簡寫 頁面對象模型。
作用:就是把測試頁面和測試腳本進行分離,即把頁面封裝成類,供測試腳本進行調用。
分層機制,讓不同層去做不同類型的事情,讓代碼結構清晰,增加復用性。
PO設計模式是Selenium自動化測試中最佳的設計模式之一,主要體現在對界面交互細節的封裝。
PO是什么?
1、頁面對象模型(PO)是一種設計模式,用來管理維護一組web元素的對象庫。
2、在PO下,應用程序的每一個頁面都有一個對應的page class。
3、每一個page class維護着該web頁的元素集和操作這些元素的方法。
4、page class中的方法命名最好根據對應的業務場景進行命名。
PO的優勢?
1、PO提供了一種業務流程與頁面元素操作分離的模式,這使得測試代碼變得更加清晰。
2、頁面對象與用例分離,使得我們更好的復用對象。
3、可復用的頁面方法代碼會變得更加優化。
4、更加有效的命名方式使得我們更加清晰的知道方法所操作的UI元素。
Page Object模式具有以下幾個優點
該觀點來自 《Selenium自動化測試——基於Python語言》
①抽象出對象可以最大程度地降低開發人員修改頁面代碼對測試的影響, 所以, 你僅需要對頁面對象進行調整, 而對測試沒有影響;
②可以在多個測試用例中復用一部分測試代碼;
③測試代碼變得更易讀、 靈活、 可維護。
Page Object模式圖

- basepage ——selenium的基類,對selenium的方法進行封裝
- pageelements——頁面元素,把頁面元素單獨提取出來,放入一個文件中
- searchpage ——頁面對象類,把selenium方法和頁面元素進行整合
- testcase ——使用pytest對整合的searchpage進行測試用例編寫
總結:通過上圖我們可以看出,通過POM模型思想,我們把:
- selenium方法
- 頁面元素
- 頁面對象
- 測試用例
以上四種代碼主體進行了拆分,雖然在用例很少的情況下做會增加代碼,但是當用例多的時候意義很大,代碼量會在用例增加的時候顯著減少。我們維護代碼變得更加直觀明顯,代碼可讀性也變得比工廠模式強很多,代碼復用率也極大的得到了提高。
不使用PO設計會出現以下幾種情況
復用性不太好,擴展性不好,易讀性差,不好維護,UI界面頻繁的項目維護起來比較麻煩。
PO模式的優缺點
優點:
提高代碼的可讀性。
減少了代碼的重復。
提高代碼的可維護性,特別是針對UI界面頻繁的項目。
缺點:
造成項目結構比較復雜,因為是根據流程進行了模塊化處理。
代碼
代碼結構:

base層
base.py
import allure from selenium.webdriver.support.wait import WebDriverWait from tools.get_log import GetLog log = GetLog.get_logger() class Base: # 初始化 def __init__(self, driver): log.info("正在初始化driver: {}".format(driver)) """解決driver""" self.driver = driver # 查找 方法封裝 def base_find(self, loc, timeout=30, poll=0.5): """ :param loc: 格式為列表或元祖,內容:元素定位信息使用By類 :param timeout: 查找元素超時時間,默認 30秒 :param poll: 查找元素頻率 默認為0.5 :return: 元素 """ log.info("正在查找元素:{}".format(loc)) return (WebDriverWait(self.driver, timeout=timeout, poll_frequency=poll).until(lambda x: x.find_element(*loc))) # 輸入 方法封裝 def base_input(self, loc, value): """ :param loc: 元素的定位信息 :param value: 要輸入的值 """ # 1. 獲取元素 el = self.base_find(loc) # 2. 清空操作 log.info("正在對:{} 元素執行清空操作!".format(loc)) el.clear() # 3. 輸入操作 log.info("正在對:{} 元素執行輸入:{} 操作!".format(loc, value)) el.send_keys(value) # 點擊 方法封裝 def base_click(self, loc): """ :param loc: 元素定位信息 """ log.info("正在對:{} 元素執行點擊操作!".format(loc)) # 獲取元素並點擊 self.base_find(loc).click() # 獲取 元素文本 def base_get_text(self, loc): """ :param loc: 元素定位信息 :return: 返回元素的文本值 """ log.info("正在對:{} 元素獲取文本操作!,獲取的文本值:{}".format(loc, self.base_find(loc).text)) return self.base_find(loc).text # 截圖 def base_get_img(self): # 1. 調用截圖方法 self.driver.get_screenshot_as_file("./image/err.png") # 2. 調用圖片寫入報告方法 self.__base_write_img() # 將圖片寫入報告方法(私有) def __base_write_img(self): # 1. 獲取圖片文件流 with open("./image/err.png", "rb") as f: # 2. 調用allure.attach附加方法 allure.attach("錯誤原因:", f.read(), allure.attach_type.PNG)
web_base.py
from time import sleep from selenium.webdriver.common.by import By from base.base import Base from tools.get_log import GetLog log = GetLog.get_logger() class WebBase(Base): """以下為你web項目專屬方法""" # 根據顯示文本點擊指定元素 def web_base_click_element(self, placeholder_text, click_text): log.info("正在調用web專屬點擊封裝方法") # 1. 點擊復選框 loc = By.CSS_SELECTOR, "[placeholder='{}']".format(placeholder_text) self.base_click(loc) # 2. 暫停 sleep(1) # 3. 點擊包含顯示文本的元素 loc = By.XPATH, "//*[text()='{}']".format(click_text) self.base_click(loc) # 判斷頁面是否包含指定元素 def web_base_is_exist(self, text): log.info("正在調用查找頁面是否存在指定元素:{} 方法".format(text)) # 1. 組裝元素配置信息 loc = By.XPATH, "//*[text()='{}']".format(text) # 2. 找元素 try: # 1. 找元素 修改查找元素時間 3 self.base_find(loc, timeout=3) # 2. 輸出找到信息 print("找到:{} 元素啦!".format(loc)) # 3. 返回 True return True except: # 1. 輸出未找到信息 print("沒有找到:{} 元素!".format(loc)) # 2. 返回False return False
app_base.py
from selenium.webdriver.common.by import By from selenium.common.exceptions import NoSuchElementException from base.base import Base from tools.get_log import GetLog from time import sleep log = GetLog.get_logger() class AppBase(Base): # 1. 查找頁面是否存在指定元素 def app_base_is_exist(self, loc): try: # 1. 調用查找方法 -> 3 self.base_find(loc, timeout=3) log.info("在app頁面中找到指定元素!") # 2. 輸出信息 print("找到:{}元素啦!".format(loc)) # 3. 返回True return True except: log.error("沒有在app頁面中找到指定元素!") # 1. 輸出信息 print("未找到:{}元素!".format(loc)) # 2. 返回False return False # 2. 從右向左滑動屏幕 def app_base_right_wipe_left(self, loc_area, click_text): log.info("正在調用從右向左滑動屏幕方法") # 1. 查找區域元素 el = self.base_find(loc_area) # 2. 獲取區域元素的位置 y坐標點 y = el.location.get("y") # 3. 獲取區域元素寬高 width = el.size.get("width") height = el.size.get("height") # 4. 計算 start_x, start_y, end_x, end_y start_x = width * 0.8 start_y = y + height * 0.5 end_x = width * 0.2 end_y = y + height * 0.5 # 組合頻道元素配置信息 # loc = By.XPATH, "//*[@class='android.widget.HorizontalScrollView']//*[contains(@text,'{}')]".format(click_text) loc = By.XPATH, "//android.widget.HorizontalScrollView/*[contains(@text,'{}')]".format(click_text) # 5. 循環操作 while True: # 1. 獲取當前屏幕頁面結構 page_source = self.driver.page_source # 2. 捕獲異常 try: sleep(2) # 1. 查找元素 el = self.base_find(loc, timeout=3) # 2. 輸出提示信息 print("找到:{} 元素啦!".format(loc)) sleep(2) # 3. 點擊元素 el.click() # 4. 跳出循環 break # 3. 處理異常 except: # 1. 輸出提示信息 print("未找到:{}元素!".format(loc)) # 2. 滑動屏幕 self.driver.swipe(start_x, start_y, end_x, end_y, duration=2000) # 4. 判斷是否為最后一頁 if page_source == self.driver.page_source: # 1. 輸出提示信息 print("滑到最后一屏幕,未到找元素!") # 2. 拋出未找到元素異常 raise NoSuchElementException # 3. 從下向上滑動屏幕 def app_base_down_wipe_up(self, loc_area, click_text): log.info("正在調用從下向上滑動屏幕方法") # 1. 查找區域元素 el = self.base_find(loc_area) # 2. 獲取區域元素寬高 width = el.size.get("width") height = el.size.get("height") # 3. 計算 start_x, start_y, end_x, end_y start_x = width * 0.5 start_y = height * 0.8 end_x = width * 0.5 end_y = height * 0.2 # 組合頻道元素配置信息 loc = By.XPATH, "//*[@bounds='[0,520][1440,2288]']//*[contains(@text,'{}')]".format(click_text) # 5. 循環操作 while True: # 1. 獲取當前屏幕頁面結構 page_source = self.driver.page_source # 2. 捕獲異常 try: # 1. 查找元素 el = self.base_find(loc, timeout=3) # 2. 輸出提示信息 print("找到:{} 元素啦!,文章標題為:{}".format(loc, el.text)) # 3. 點擊元素 el.click() # 4. 跳出循環 break # 3. 處理異常 except: # 1. 輸出提示信息 print("未找到:{}元素!".format(loc)) # 2. 滑動屏幕 self.driver.swipe(start_x, start_y, end_x, end_y, duration=2000) # 4. 判斷是否為最后一頁 if page_source == self.driver.page_source: # 1. 輸出提示信息 print("滑到最后一屏幕,未到找元素!") # 2. 拋出未找到元素異常 raise NoSuchElementException
page層
page_mis_login.py
from base.web_base import WebBase import page from tools.get_log import GetLog log = GetLog.get_logger() class PageMisLogin(WebBase): # 1. 輸入用戶名 def page_input_username(self, username): self.base_input(page.mis_username, username) # 2. 輸入密碼 def page_input_pwd(self, pwd): self.base_input(page.mis_pwd, pwd) # 3. 點擊登錄按鈕 def page_click_login_btn(self): # 1. 處理js js = "document.getElementById('inp1').disabled=false" self.driver.execute_script(js) # 2. 調用點擊操作 self.base_click(page.mis_login_btn) # 4. 獲取昵稱封裝 def page_get_nickname(self): return self.base_get_text(page.mis_nickname) # 5. 組合后台管理登錄業務方法 def page_mis_login(self, username, pwd): log.info("正在調用后台管理系統登錄業務方法,用戶名:{} 密碼:{}".format(username, pwd)) self.page_input_username(username) self.page_input_pwd(pwd) self.page_click_login_btn() # 6. 組合后台管理登錄業務方法 def page_mis_login_success(self, username="testid", pwd="testpwd123"): log.info("正在調用后台管理系統成功登錄依賴方法,用戶名:{} 密碼:{}".format(username, pwd)) self.page_input_username(username) self.page_input_pwd(pwd) self.page_click_login_btn()
page_mis_audit.py
from base.web_base import WebBase from time import sleep import page from tools.get_log import GetLog log = GetLog.get_logger() class PageMisAudit(WebBase): # 文章id article_id = None # 1. 點擊信息管理 def page_click_info_manage(self): # 1. 暫停時間 sleep(1) # 2. 點擊信息管理 self.base_click(page.mis_info_manage) # 2. 點擊內容審核 def page_click_content_audit(self): # 1. 暫停時間 sleep(1) # 2. 點擊內容審核 self.base_click(page.mis_content_audit) # 3. 輸入文章標題 def page_input_title(self, title): self.base_input(page.mis_title, title) # 4. 輸入文章頻道 def page_input_channel(self, channel): self.base_input(page.mis_channel, channel) # 5. 選擇狀態 def page_click_status(self, placeholder_text="請選擇狀態", click_text="待審核"): self.web_base_click_element(placeholder_text, click_text) # 6. 點擊查詢按鈕 def page_click_find(self): # 1. 點擊查詢按鈕 self.base_click(page.mis_find) # 2. 暫停時間 sleep(2) # 7. 獲取文章id def page_get_article_id(self): return self.base_get_text(page.mis_article_id) # 8. 點擊通過 def page_click_pass_btn(self): self.base_click(page.mis_pass) # 9. 點擊確認 def page_click_confirm_pass(self): # 1. 暫停時間 sleep(1) # 點擊確認 self.base_click(page.mis_confirm_pass) # 10. 組合發布文章業務方法 def page_mis_audit(self, title, channel): log.info("正在調用審核文章業務方法,title: {} channel:{}".format(title, channel)) self.page_click_info_manage() self.page_click_content_audit() self.page_input_title(title) self.page_input_channel(channel) self.page_click_status() self.page_click_find() self.article_id = self.page_get_article_id() print("獲取的文章id為:", self.article_id) self.page_click_pass_btn() self.page_click_confirm_pass() # 11. 組裝斷言業務操作方法 def page_assert_audit(self): log.info("正在調用斷言業務操作方法") # 1. 暫停3秒 sleep(3) # 2. 修改狀態 ->審核通過 self.page_click_status(click_text="審核通過") # 3. 點擊查詢按鈕 self.page_click_find() # 4. 判斷當前頁面是否存在指定元素 並 返回結果 return self.web_base_is_exist(self.article_id)
page_app_login.py
from base.app_base import AppBase import page from time import sleep from tools.get_log import GetLog log = GetLog.get_logger() class PageAppLogin(AppBase): # 1. 輸入手機號 def page_input_phone(self, phone): self.base_input(page.app_phone, phone) # 2. 輸入驗證碼 def page_input_code(self, code): self.base_input(page.app_code, code) # 3. 點擊登錄按鈕 def page_click_login_btn(self): # 建議等待1-2秒 sleep(2) self.base_click(page.app_login_btn) # 4. 判斷頁面是否存在 我的菜單 def page_is_login_success(self): return self.app_base_is_exist(page.app_me) # 5. 組合登錄業務方法 def page_app_login(self, phone, code): log.info("正在調用app應用登錄業務方法 手機號:{} 驗證碼: {}".format(phone, code)) self.page_input_phone(phone) self.page_input_code(code) self.page_click_login_btn() # 6. 組合登錄依賴成功業務方法 def page_app_login_success(self, phone="13812345678", code="246810"): log.info("正在調用app應用登錄業務方法 手機號:{} 驗證碼: {}".format(phone, code)) self.page_input_phone(phone) self.page_input_code(code) self.page_click_login_btn()
page_app_article.py
from base.app_base import AppBase import page from tools.get_log import GetLog log = GetLog.get_logger() class PageAppArticle(AppBase): # 1. 查找頻道 def page_click_channel(self, click_text): # 調用 從右向左滑動方法 self.app_base_right_wipe_left(page.app_channel_area, click_text) # 2. 查找文章 def page_click_article(self, title): # 調用 從下向上滑動方法 self.app_base_down_wipe_up(page.app_article_area, click_text=title) # 3. 查找文章業務方法 def page_app_article(self, find_text, title): log.info("正在調用查詢文章業務方法 文章頻道:{} 文章title:{}".format(find_text, title)) # 1. 調用查找頻道 self.page_click_channel(find_text) # 2. 調用查找文章 self.page_click_article(title)
封裝統一入口類:page_in.py
from page.page_app_article import PageAppArticle from page.page_app_login import PageAppLogin from page.page_mis_audit import PageMisAudit from page.page_mis_login import PageMisLogin from page.page_mp_article import PageMpArticle from page.page_mp_login import PageMpLogin class PageIn: def __init__(self, driver): self.driver = driver # 獲取PageMpLogin對象 def page_get_PageMpLogin(self): return PageMpLogin(self.driver) # 獲取PageMpArticle對象 def page_get_PageMpArticle(self): return PageMpArticle(self.driver) # 獲取PageMisLogin對象 def page_get_PageMisLogin(self): return PageMisLogin(self.driver) # 獲取PageMisAudit對象 def page_get_PageMisAudit(self): return PageMisAudit(self.driver) # 獲取PageAppLogin對象 def page_get_PageAppLogin(self): return PageAppLogin(self.driver) # 獲取PageAppArticle對象 def page_get_PageAppArticle(self): return PageAppArticle(self.driver)
scripts層
test01_mp_login.py
import pytest from tools.get_log import GetLog from page.page_in import PageIn from tools.get_driver import GetDriver import page from tools.read_yaml import read_yaml log = GetLog.get_logger() class TestMpLogin: # 初始化 def setup_class(self): # 1. 獲取driver driver = GetDriver.get_web_driver(page.url_mp) # 2. 通過統一入口類獲取PageMpLogin對象 self.mp = PageIn(driver).page_get_PageMpLogin() # 結束 def teardown_class(self): # 調用關閉driver GetDriver.quit_web_driver() # 測試業務方法 @pytest.mark.parametrize("username,code,expect", read_yaml("mp_login.yaml")) def test_mp_login(self, username, code, expect): # 調用登錄業務方法 self.mp.page_mp_login(username, code) try: # 斷言 assert expect == self.mp.page_get_nickname() except Exception as e: log.error("斷言出錯,錯誤信息:{}".format(e)) print("錯誤原因:", e) # 截圖 self.mp.base_get_img() # 拋異常 raise
test02_mp_article.py
import pytest import page from page.page_in import PageIn from tools.get_driver import GetDriver from tools.get_log import GetLog from tools.read_yaml import read_yaml log = GetLog.get_logger() class TestMpArticle: # 1. 初始化 def setup_class(self): # 1. 獲取driver driver = GetDriver.get_web_driver(page.url_mp) # 2. 獲取統一入口類對象 self.page_in = PageIn(driver) # 3. 獲取PageMpLogin對象並調用成功登錄依賴方法 self.page_in.page_get_PageMpLogin().page_mp_login_success() # 4. 獲取PageMpArticle頁面對象 self.article = self.page_in.page_get_PageMpArticle() # 2. 結束 def teardown_class(self): GetDriver.quit_web_driver() # 3. 測試發布文章方法 @pytest.mark.parametrize("title,content,expect,channel", read_yaml("mp_article.yaml")) def test_mp_article(self, title, content, expect, channel): print("發布文章所屬頻道為:", channel) # 調用發布文章業務方法 self.article.page_mp_article(title, content) try: # 斷言 assert expect == self.article.page_get_info() except Exception as e: # 日志 log.error(e) # 截圖 self.article.base_get_img() # 拋異常 raise
test05_app_login.py
import pytest from page.page_in import PageIn from tools.get_driver import GetDriver from tools.get_log import GetLog from tools.read_yaml import read_yaml log = GetLog.get_logger() class TestAppLogin: # 1. 初始化 def setup_class(self): # 1. 獲取driver driver = GetDriver.get_app_driver() # 2. 通過統一入口對象獲取PageAppLogin對象 self.login = PageIn(driver).page_get_PageAppLogin() # 2. 結束 def teardown_class(self): # 關閉driver GetDriver.quit_app_driver() # 3. app登錄測試業務方法 @pytest.mark.parametrize("phone,code", read_yaml("app_login.yaml")) def test_app_login(self, phone, code): # 調用app登錄業務方法 self.login.page_app_login(phone, code) try: # 斷言 assert self.login.page_is_login_success() except Exception as e: # 1. 日志 log.error(e) # 2. 截圖 self.login.base_get_img() # 3. 拋異常 raise
test06_app_article.py
import pytest from page.page_in import PageIn from tools.get_driver import GetDriver from tools.get_log import GetLog from tools.read_yaml import read_yaml log = GetLog.get_logger() class TestAppLogin: # 1. 初始化 def setup_class(self): # 1. 獲取driver driver = GetDriver.get_app_driver() # 2. 通過統一入口對象獲取PageAppLogin對象 self.login = PageIn(driver).page_get_PageAppLogin() # 2. 結束 def teardown_class(self): # 關閉driver GetDriver.quit_app_driver() # 3. app登錄測試業務方法 @pytest.mark.parametrize("phone,code", read_yaml("app_login.yaml")) def test_app_login(self, phone, code): # 調用app登錄業務方法 self.login.page_app_login(phone, code) try: # 斷言 assert self.login.page_is_login_success() except Exception as e: # 1. 日志 log.error(e) # 2. 截圖 self.login.base_get_img() # 3. 拋異常 raise
