BasePage 與共用行為:把重複收進一個地方

#Page Object Model #BasePage #測試架構 #QA 實戰

你有十個 page 物件,每個都寫了一模一樣的 WebDriverWait(self.driver, 10).until(...) 。有一天 timeout 要從 10 秒改成 15 秒,你一口氣要改十個地方。 哪些行為該共用 不是每件事都要放進 BasePage,只有「每個 page 都要做」的才算。常見的有: 顯式等待後再操作 : click() 前先等 element to be clickable 、 send keys() 前先等 visibility of element located ,幾乎每個 page 都要做。 取得元件文字 :等可見再取 .text ,漏掉等待就拿到空字串。 捲動找元件 :行動端長頁面,元件不在視窗內就點不到,需要先滾過去。 context 切換 :有 WebView 的 App,需要在 NATIVE APP 與 WEBVIEW 之間切換;每個頁面切法一樣,邏輯不應散落各處。 這四類行為只要有一個地方改,全部 page 都跟著生效,這就是放進 BasePage 的判斷依據。 BasePage 範例 pages/base page.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected conditions as EC class BasePage: def init (self, driver, timeout=10): self.driver = driver self.wait = WebDriverWait(driver, timeout) def tap(self, locator): """等元件可點擊後點擊""" self.wait.until(EC.element to be clickable(locator)).click() def type(self, locator, text): """等元件可見後輸入文字""" self.wait.until( EC.visibility of element located(locator) ).send keys(text) def text of(self, locator): """等元件可見後回傳文字""" return self.wait.until( EC.visibility of element located(locator) ).text def scroll to(self, locator, max swipes=5): """往下滑直到元件出現(原生畫面沒有 scrollIntoView,只能邊滑邊找)""" for in range(max swipes): elements = self.driver.find elements( locator) if elements: return elements[0] size = self.driver.get window size() x = size["width"] // 2 h = size["height"] self.driver.swipe(x, int(h 0.7), x, int(h 0.3), 500) raise AssertionError(f"滑了 {max swipes} 次還是找不到 {locator}") def switch to webview(self): """切換到第一個 WEBVIEW context""" contexts = self.driver.contexts webview = next(c for c in contexts if "WEBVIEW" in c) self.driver.switch to.context(webview) def switch to native(self): """切回 NATIVE APP context""" self.driver.switch to.context("NATIVE APP") timeout=10 只在一個地方設定;所有等待邏輯集中在 BasePage ,子 page 不需要知道 WebDriverWait 是什麼。 注意 scroll to :Web 上常見的 scrollIntoView 是 JavaScript,在原生畫面裡不能用。原生 App 要捲到某個元件,只能「滑一下、找一次」地重複,直到找到或放棄。 page 繼承 BasePage 上一篇的 LoginPage 直接用 find element ,沒有任何等待,現在改成繼承 BasePage 、呼叫 self.tap / self.type : pages/login page.py from appium.webdriver.common.appiumby import AppiumBy from pages.base page import BasePage from pages.home page import HomePage class LoginPage(BasePage): 繼承 BasePage USERNAME = (AppiumBy.ACCESSIBILITY ID, "username") PASSWORD = (AppiumBy.ACCESSIBILITY ID, "password") LOGIN BTN = (AppiumBy.ACCESSIBILITY ID, "login btn") ERROR MSG = (AppiumBy.ACCESSIBILITY ID, "error msg") init 不需要重寫,BasePage 已經接收 driver def login(self, user: str, pw: str) > "HomePage": self.type(self.USERNAME, user) BasePage.type,含等待 self.type(self.PASSWORD, pw) self.tap(self.LOGIN BTN) BasePage.tap,含等待 return HomePage(self.driver) def error is shown(self) > bool: return self.text of(self.ERROR MSG) != "" BasePage.text of 改寫前後, LoginPage 的 login() 裡的三行 find element 換成 self.type 、 self.tap ,每一步都自帶等待;未來再加 ProfilePage 、 CartPage ,同樣的等待行為一行都不用重寫。 集中維護的價值 假設 CI 環境偏慢,timeout 要從 10 秒調到 20 秒,舊做法要改每一個 page;有了 BasePage,只改一行: conftest.py @pytest.fixture def login page(driver): return LoginPage(driver, timeout=20) 傳入 timeout,BasePage 接收 或者直接改 BasePage. init 的預設值: def init (self, driver, timeout=20): 改這一行,全部 page 生效 BasePage 自己也會變成 god object 上一篇講過首頁 page 物件變成 god object。BasePage 更誇張:我專案裡的 BasePage 曾經大到沒人讀得完。 原因跟首頁一樣:「每個 page 都會用到」這個判斷標準很寬鬆,點擊、滑動、彈窗、WebView、圖片比對、OCR,每一樣都說得通。每次遇到新的共用需求就往裡面加,最後它變成專案裡最難讀的一個檔案。 後來我把它按行為拆成一組 mixin:點擊一個、滑動一個、彈窗一個、WebView 一個……BasePage 本體只剩很薄的一層,負責把這些 mixin 組起來。子 page 的用法完全沒變,還是繼承 BasePage。 拆完之後,BasePage 加 mixin 的方法還是多到記不住。所以寫新方法之前,我一定先搜尋一下有沒有現成的。方法太多、沒人記得住,就會出現三個名字不同、做同一件事的方法。 集中的另一面:改一行,壞一片 把共用行為收進 BasePage,好處是改一處全部生效。壞處也一樣:改錯一處,全部一起壞。 我在專案裡把最常被呼叫的幾個共用方法列成「高風險函式」,例如回到首頁、找到才點、處理彈窗、滑到文字出現為止。幾乎每支測試都會經過它們,改動它們之前有固定的規矩: 高風險函式的修改規矩: 先讀專案裡的穩定性規則,照著改 Android 和 iOS 都要驗 不准改函式簽名,要擴充就加有預設值的參數 改完跑一支比對腳本,確認簽名和行為跟改之前一致 這份清單和規則,是 AI 教會我的。 有一次我讓 AI 幫忙修一支不穩定的測試。它對 root cause 的判斷錯了,順手改了其中一個高風險函式的簽名。那支測試過了,但其他用到這個函式的測試全部壞掉。我是在 review diff 的時候發現的,還好沒有進主線。 事後我沒有怪 AI。它不知道哪些函式碰不得,因為我從來沒寫下來過。那些規矩一直都只在我腦袋裡。所以我把高風險函式清單和修改規則寫進專案給 AI 讀的規則檔,下次它要改這些函式之前,會先讀到這份清單。 這件事讓我對 BasePage 的看法變了:集中維護點的同時,也集中了風險。維護點越集中,越需要寫清楚哪裡不能亂動,給人看,也給 AI 看。