Appium UI 自動化到底要不要用 Page Object 模式?(續 - 深入了解 PO 模式, 并改造 PO 模式)

小怪獸 · 2019年07月17日 · 最後由 小怪獸 回複于 2019年08月07日 · 4544 次閱讀
本帖已被設為精華帖!

背景

前幾天發布了一個話題“ UI 自動化到底要不要用 Page Object 模式?“ 原帖子:https://juhua446633.cn/topics/19831
大家給出了很多觀點, 有傾向于po模式, 也有建議根據不同項目場景自行處理,最後還是一知半解,并沒有搞懂,于是集中深入的了解了下Page Object模式

Page Object模式 python webdriver 版本

這裡介紹下我近期對PO模式的理解, 整體思想是分層,讓不同層去做不同類型的事情,讓代碼結構清晰,增加複用性
一般分兩層或三層(也有四層的):

兩層:

  • 對象邏輯層
  • 業務數據層

三層:

  • 對象庫層
  • 邏輯層
  • 業務層

四層:

  • 對象庫層
  • 邏輯層
  • 業務層
  • 數據層

不同層本質差不多
下面以登錄為例子 (網上絕大多數都是以登錄為例子,但登錄隻能讓新手明白PO大概是怎樣子,優勢卻很難傳遞出來)
普通方式:

def test_user_login():
driver = webdriver.Edge()
base_url = 'https://mail.qq.com/'
username = '3494xxxxx' # qq号碼
password = 'kemixxxx' # qq密碼
driver.get(base_url)
driver.switch_to.frame('login_frame') #切換到登錄窗口的iframe
driver.find_element(By.ID, "u").send_keys(username) #輸入賬号
driver.find_element(By.ID, "p").send_keys(password) #輸入密碼
driver.find_element(By.ID, "login_button").click() #點擊登錄

PO模式

對象庫層

#創建基礎類
class BasePage(object):
#初始化
def __init__(self, driver):
self.base_url = 'https://mail.qq.com/'
self.driver = driver
self.timeout = 30

#打開頁面
def _open(self):
url = self.base_url
self.driver.get(url)
self.driver.switch_to.frame('login_frame') #切換到登錄窗口的iframe

def open(self):
self._open()

#定位方法封裝
def find_element(self,*loc):
return self.driver.find_element(*loc)
#創建LoginPage
class LoginPage(BasePage):
username_loc = (By.ID, "u")
password_loc = (By.ID, "p")
login_loc = (By.ID, "login_button")

#輸入用戶名
def type_username(self,username):
self.find_element(*self.username_loc).send_keys(username)

#輸入密碼
def type_password(self,password):
self.find_element(*self.password_loc).send_keys(password)

#點擊登錄
def type_login(self):
self.find_element(*self.login_loc).click()

邏輯層

#創建test_user_login()函數
def user_login(driver, username, password):
"""測試用戶名/密碼是否可以登錄"""
login_page = LoginPage(driver)
login_page.open()
login_page.type_username(username)
login_page.type_password(password)
login_page.type_login()

業務層

def test_user_login():
driver = webdriver.Edge()
username = '3494xxxxx' #qq号碼
password = 'kemixxxx' #qq密碼
test_user_login(driver, username, password)

分析

一 代碼量多了大概三倍, 代碼量增加是一定的先忽略,後面重點讨論

二 分層之後真的易于維護嗎?

我們來看下當元素發生變化的時候,隻需要在對象庫層找打對應元素修改。 咦? 你會說普通方式不也一樣嗎, 看上去一樣,其實有細微差異,而一些細微差異會導緻很大不同

  • 效率高 : PO模式每個元素有變量定義,更方便查找。 而普通方式得通過備注或上下文來推斷效率低。 ps:随着case不斷增加,海量元素的定義對于英語一般的同學挑戰也大,有人說有谷歌翻譯。定義的時候可以通過翻譯, 但到時候回過來查過元素怎麼辦? 翻譯通常是1對多,我們當時選哪個? 用哪個來搜索? 這或許也是海量變量定義帶來的困擾
  • 複用多收益大: 當某個元素被多次引用的時候,隻需要修改一處便可,而普通方式需要一處一處找出來并修改,可以看出來複用越多PO模式收益越大

當界面需求發生變化

  • 1.新增或删除了一些功能點或調整操作步驟先後順序,但上層業務不變
    • 效率高 :同理,PO模式的邏輯層方法有具體定義,情況和元素發生變化一樣 修改邏輯層,業務層不變。這樣看來結構簡單清晰,舒服更符合人類習慣, 普通方式就是繼續堆case
    • 複用多收益大: 同樣這裡如果邏輯複用越多,PO模式收益越大,因為對于PO模式來說都隻需要修改一個地方多處受益
    1. 上層業務發生變化

    看上去兩者差異不大

小結

總得來看:

  • case越多使用PO模式會使你的代碼結構更清晰
  • 元素複用越多PO模式下維護非常容易
  • 邏輯複用越多PO模式下維護非常容易 (如果邏輯複用多,需要多考慮邏輯層的顆粒度)
  • 元素/邏輯/數據複用越多應選擇更多層的PO模式
PO模式 普通方式
代碼量 a*N(a>=2) N
可閱讀性 很差
維護性

好,我們再回過頭來看看代碼量大的問題,有沒有辦法精簡一些呢? 把 a*N 中的a變成1.8, 1.5, 1.2, 甚至接近1 呢?
開始下一輪探索:

探索 代碼量大的問題

以三層PO為例我們大概的流程是這樣的:
在對象庫層,我們定義了元素,再為元素定義了一些基本的操作流, 在邏輯層 為集成了基本操作流,在業務層 組裝邏輯 和 數據輸入
看上去 第二 第三步驟 有點重複 能不能去掉? 如果隻剩下第一 四個步驟 那代碼量瞬間就下來了 那該有多爽

試試看

如果去掉第二/三步驟,那意味着我們隻需要定義元素,并在業務層需要指定操作的時候再自動生成對應所需操作。即需要時生成,用完後丢棄。
這裡需要用到python下面的魔法方法 "getattribute"
思路:
在訪問類App屬性時擋截下來,曆遍對象庫層找到對應元素返回對應的對象類App.LoginPage,而對象庫層都繼承了BasePage類,在BasePage中同樣重構了"getattribute",當App.LoginPage對象嘗試調用click()之類的方法時,就臨時綁定click方法(click/swip/get_text/set_text.....)。

這樣做的話,就隻需要編寫元素對象庫,在 業務層直接自由調用,即時生成,用完丢棄。 代碼量大幅減少

對象庫層 (這裡使用airtest下面的poco控件識别框架舉例,和Appium Selenium 略微不同)

class AndroidHomePage(BasePage):
def __init__(self, driver):
super().__init__(driver)

self.p_account= "NormalWindow/AccountInputField"
self.p_password = "NormalWindow/PwdInputField"

業務層

def test_login(id,pw)
App.LoginPage.p_account.set_text(id)
App.LoginPage.p_password.set_text(pw)

如此看來用例編寫者就更接近隻需要關注業務

以下是關鍵思路的實現 以App類為例子 BasePage Element 大概相同

  • App類

    def __getattribute__(self, attr):
    """
    擋截屬性訪問
    """

    target_page = None
    if attr.endswith('page'): # 過濾page
    page = import_module(attr) #曆遍 對象庫層目錄src/page 找到目标文件

    if self.client_version == CHINA_PLATFORM: # 國内版本
    for item in page.__dict__:
    if item.startswith(CHINA_PAGE_PREFIX) :
    target_page = getattr(page, item)
    elif self.client_version == OVERSEAS_PLATFORM: # 海外版本
    for item in page.__dict__:
    if item.startswith(OVERSEAS_PAGE_PREFIX) :
    target_page = getattr(page, item)

    return target_page(self._driver)
    else:
    # 非過濾直接訪問
    return object.__getattribute__(self, attr)

    結尾

    以上這近幾天對PO的新認識,歡迎大家多多指點

共收到 27 條回複 時間 點贊

學習了

3樓 已删除

不是哦 這裡是參考的這個思路 很棒

暫時放棄ui,感覺接口上手更簡單

陳恒捷 将本帖設為了精華貼 07月18日 11:02
9樓 已删除

我通常是在page類定義各元素、以及各元素操作方法;case類傳參調用元素操作方法

cheunghr 回複

恩 你這是兩層PO了

總結的很好啊,個人覺得PO模式好處還是很大的

不知不覺,自己寫的就是你們說的PO模型

看來我寫的還不算是PO啊,少了邏輯層,邏輯和業務放在一塊的,考慮的是某些同學不在項目中,可能不熟悉業務,讓項目中的人自己去寫業務和邏輯了

層次清晰,便于維護。

#輸入密碼
def type_password(self,password):
self.find_element(*self.password_loc).send_keys(password)

請問一下,這裡 self.password_loc 中的 是什麼作用,百度了的内容感覺沒法套用進來

植樹人 回複


解包的意思,可以粗劣的認為是去掉 “()”

樓主總結的很好,個人覺得應該按業務模塊來分層較好,比如登錄,可能涉及多個頁面,但隻寫到一個登錄模塊去。

原來我一直寫的就是PO。。。

能出一個詳細的整體的例子嗎?就拿郵箱登陸

看前端技術吧,現在都是組件化,在組件複用情況下,先模塊對象,再頁面對象,反而更合理,頁面如果頻繁變動(邏輯),你還是怎麼簡單怎麼來。能錄制就錄制

再說ui本身偏業務,把業務描述轉為行為代碼應該才是優化的點

可以的! 樓主 是自己想的嗎==


請問這個return 怎麼理解== 不是很明白

wengzexiong 回複

敲錯啦,應該是target_page 。已經修改

兩層用着還是可以的。

多麼簡潔的代碼!連我都能看懂!公司第一工程師封給你。

推薦一個BasePageObject eeaston/page-objects

excuter611 第十期_selenium 進階_20190728 中提及了此貼 08月02日 21:28
excuter611 第十期_selenium 進階_20190728 中提及了此貼 08月03日 14:52

樓主很棒,看起來也講得很清楚,但是對于屬性攔截生成那一塊看得一知半解,能夠給一個完整的 demo 例子代碼?感激不盡!

xiaomingpapapa 回複

好,明天把代碼貼上去

需要 登錄 後方可回複, 如果你還沒有賬号請點擊這裡 注冊
http://m.juhua446633.cn|http://wap.juhua446633.cn|http://www.juhua446633.cn||http://juhua446633.cn