設計一個 Page 物件:元素、動作、回傳下一頁

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

第一次寫 page 物件,很多人會把 locator 放錯地方、動作命名含糊、忘記回傳下一頁,等測試串不起來才發現。一個 page 物件該有的,就是下面三樣東西。 page 物件的三個組成 一個設計良好的 page 物件只包含三件事: 元素 locator(class 常數) :用 tuple 存起來,找元件只要解包 ,不重複寫 AppiumBy 。 語意化的動作方法 :方法名稱說的是「做什麼」,不是「點哪個 id」: login() 就比 click login btn() 好。 回傳下一個 page 物件 :動作後 UI 跳頁,method 就回傳對應的 page class;讓 test 能串成流程,不用手動建下一頁物件。 page.click login btn() 說的是點哪個元件,test 讀起來像操作手冊。 page.login("demo", "pw") 說的是要做什麼,按鈕換名字也不用改 test。 登入頁範例 pages/login page.py from appium.webdriver.common.appiumby import AppiumBy class LoginPage: 1. locator 集中在 class 常數 USERNAME = (AppiumBy.ACCESSIBILITY ID, "username") PASSWORD = (AppiumBy.ACCESSIBILITY ID, "password") LOGIN BTN = (AppiumBy.ACCESSIBILITY ID, "login btn") ERROR MSG = (AppiumBy.ACCESSIBILITY ID, "error msg") def init (self, driver): self.driver = driver 2. 語意化動作方法 def login(self, user: str, pw: str) > "HomePage": self.driver.find element( self.USERNAME).send keys(user) self.driver.find element( self.PASSWORD).send keys(pw) self.driver.find element( self.LOGIN BTN).click() 3. 回傳下一頁 page 物件 return HomePage(self.driver) def error is shown(self) > bool: return self.driver.find element( self.ERROR MSG).is displayed() locator 全都在最上面,看一眼就知道這個頁面管了哪些元件; find element( self.USERNAME) 用解包,不需要重複寫策略字串。 動作回傳下一頁 login() 回傳 HomePage ,是整個設計的關鍵。沒有這個回傳,test 要自己 new 下一頁物件,流程一長就散掉。 有了回傳,test 可以直接鏈結: home = LoginPage(driver).login("demo", "pw123") home 現在是 HomePage 物件,可以繼續操作 profile = home.go to profile() 這種寫法讓操作流程讀起來像一句話:「登入,然後去個人頁」。 測試怎麼用 test 只做 assert,完全不碰 locator: tests/test login.py from pages.login page import LoginPage from pages.home page import HomePage def test login success(driver): home = LoginPage(driver).login("demo", "pw123") assert home.welcome message is displayed() 斷言在 test,不在 page def test login wrong password(driver): 登入失敗不跳頁,頁面仍在 LoginPage,所以直接對同一個物件查詢狀態 login = LoginPage(driver) login.login("demo", "wrong pw") assert login.error is shown() 兩個 test 都不需要 import AppiumBy ,所有 locator 都藏在 LoginPage 裡。test 讀起來就是業務語言:登入成功驗歡迎訊息、登入失敗驗錯誤提示,沒有任何定位細節外漏。 失敗訊息也更清楚:如果 login() 噴錯,代表操作壞了;如果 assert 失敗,代表預期行為沒達到。兩件事分得很開。 我專案裡的 page 物件長這樣 上面是教科書版本。我自己的框架有幾個地方做法不太一樣,也多了幾條規矩。 locator 不寫在 class 裡,寫在 yml。 一頁一個 page 物件,檔名 xxx page.py ,對應一份 element locators/xxx page.yml ,初始化時自動載入。UI 改版只動 yml,不動程式。新頁面我用一支腳手架腳本產生,page 物件和 yml 一次建好,檔名和結構不會每個人寫得不一樣。 class StorePage(BasePage): def init (self, appium driver, device, take screenshot, rp logger, request): super(). init (appium driver, device, take screenshot, rp logger, request) 自動讀 element locators/store page.yml self. auto load locators() @log step(step description="購買指定商品") def buy item(self, item name: str) > bool: self.wait for element(name="store tab", timeout=10) self.tap element by locator( element="store tab", locators=self.locators) if self.platform == "android": self.tap element by locator( element="buy btn", locators=self.locators) else: self.click ios static text containing(keyword=item name) return self.element exists("buy success text") 動作回傳布林值,不回傳下一頁。 我的 page 物件大多回傳「這個動作有沒有成功」,由測試去 assert;下一頁的物件由測試自己建立。這跟上面教的「回傳下一頁」不一樣,兩種都有人用,重點是整個專案選一種、一致就好。 雙平台差異吸收在 page 物件裡。 Android 和 iOS 的操作不同時,用 if self.platform == "android": 在 page 物件裡分開處理。測試層不應該出現平台判斷。 每個對外方法掛 @log step 。 這個 decorator 會把步驟記進報表,測試失敗時打開 ReportPortal,就能看到停在哪一步。沒掛的方法,在報表上就是一片空白。 不改既有方法的簽名。 已經有測試在用的方法,要擴充就加一個有預設值的新參數,或乾脆加新方法。直接改簽名,別的測試會在你不知道的地方壞掉。這條規矩是被 AI 教會的,後面講 BasePage 的那篇會細說。 反面教材:什麼都管的 page 物件 規矩寫起來簡單,守起來難。我專案裡最大的 page 物件是首頁,檔案長到捲不完, if platform 分支到處都是。 它不是一開始就這麼大的。首頁是 App 的核心,幾乎每個流程都會經過,每次新功能上線,最方便的做法就是在首頁的 page 物件裡再加一個方法。加著加著,它就變成一個什麼都管的 god object:改一個地方,要擔心其他一大堆平台分支會不會受影響。 回頭看,問題出在「一頁一個 page 物件」這條規則,遇到一個什麼都放的頁面時就不夠用了。首頁上的每一塊區域(彈窗、選單、底部導覽)其實都應該是自己的元件物件。這個債怎麼還,我在 POM 進階那篇再講。