「把所有的 Locator 和 Click 動作塞進一個叫
LoginPage.ts的檔案裡,並不等於你學會了 Page Object;你可能只是把混亂從測試腳本搬到了頁面類別裡。」
大家好,我是 Jane,一個每天跟 DOM 結構、CSS 定位器與 Web 介面改版奮戰的 Web UI 自動化 QA。
在上集 Day 8 中,我們聊到了測試程式碼的品質與壞氣味。當我們開始做 Web UI 自動化時,大家第一個接觸到的「設計模式(Design Pattern)」通常就是 Page Object Model (POM)。
POM 的核心理念非常美好:將『頁面結構與操作』與『測試案例與斷言』分離。
這樣當前端改版、UI 元素位置改變時,我們只需要修改對應的 Page Object,而不需要動到幾十個 Test Cases。
然而,當專案規模變大、Web 頁面越來越複雜(各種 Modal、Navbar、Sidebar、動態元件)時,許多 QA 卻把 POM 用成了一場災難:
今天這篇文章,我們就來好好聊聊:Page Object 的真正目的是什麼?我們該如何避免濫用?以及何時該改用 Component Object Pattern?
在看正確作法前,我們先來看看第一線 QA 最常踩到的 3 個 POM 濫用陷阱:
┌─────────────────────────┐
│ Page Object 常見濫用陷阱│
└────────────┬────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
1. 演變成上帝物件 (God Object) 2. 職責混淆 (Assertion Leak) 3. 鏈式呼叫地獄 (Method Chaining)
後台管理系統(Dashboard)或電商首頁,頁面上包含導覽列、搜尋列、商品列表、輪播 Banner、側邊欄、頁尾……如果全部寫在 HomePage.ts 裡,這個檔案很快就會膨脹到 2000 多行。維護它比維護測試腳本還痛苦!
把 expect(element).toBeVisible() 或 assert title == "..." 寫在 Page Object 的方法內。
為了讓測試案例寫起來像 page.goto().fillUser().clickSubmit().expectSuccess(),強迫每個 Page 方法都要回傳下一個頁面的 PageObject。
一個健康的 Web UI 自動化架構,應該分成三個層級:
┌─────────────────────────────────────────────────────────────┐
│ 1. Test Case 層 (測試意圖與斷言) │
│ - 負責:Arrange - Act - Assert、商業期待斷言 │
├─────────────────────────────────────────────────────────────┤
│ 2. Business Flow / Service 層 (業務流程組合 - 選填) │
│ - 負責:將多個 UI 步驟組合為商業服務 (如: completeCheckout)│
├─────────────────────────────────────────────────────────────┤
│ 3. Page Object / Component Object 層 (UI 操作與元素封裝) │
│ - 負責:Locator 管理、元素基本操作 (click, fill, get) │
└─────────────────────────────────────────────────────────────┘
expect() 或 assert」(除非是檢查頁面是否已載入完成的內部狀態判斷)。page.click('.btn-submit'))。為了解決大型頁面導致 Page Object 肥大的問題,現代 Web UI 自動化更推崇 Component Object Model(元件物件模式)。
Web 發展到 React / Vue / Angular 後,前端早已不是以「頁面(Page)」為單位,而是以「元件(Component)」為單位。測試架構也應該跟進!
將頁面上重複出現或獨立的區塊,抽離成獨立的 Component 物件,例如:
NavbarComponent(頂部導覽列)ModalComponent(通用對話框/彈窗)TableComponent(資料表格與分頁)CartDrawerComponent(側滑購物車)Python
# pages/dashboard_page.py - 全部塞在一起
from playwright.sync_api import Page
class DashboardPage:
def __init__(self, page: Page):
self.page = page
# 頂部導覽列操作
def click_user_profile(self):
self.page.click("#user-profile")
def logout(self):
self.page.click("#logout-btn")
# 側邊欄操作
def navigate_to_settings(self):
self.page.click("#sidebar-settings")
# 主要表格操作
def search_table(self, keyword: str):
self.page.fill("#search-input", keyword)
self.page.click("#search-btn")
def get_table_row_count(self) -> int:
return self.page.locator("table.data-table tbody tr").count()
Python
# pages/components/navbar_component.py
from playwright.sync_api import Page
class NavbarComponent:
def __init__(self, page: Page):
self.page = page
def logout(self):
self.page.click("#logout-btn")
# pages/components/table_component.py
from playwright.sync_api import Locator
class TableComponent:
def __init__(self, container: Locator):
# 將操作限制在特定的 Locator (DOM 容器) 範圍內
self.container = container
def search(self, keyword: str):
self.container.locator("#search-input").fill(keyword)
self.container.locator("#search-btn").click()
def get_rows(self) -> Locator:
return self.container.locator("tbody tr")
# pages/dashboard_page.py - 組合元件
from playwright.sync_api import Page
from pages.components.navbar_component import NavbarComponent
from pages.components.table_component import TableComponent
class DashboardPage:
def __init__(self, page: Page):
self.page = page
self.navbar = NavbarComponent(page)
# 將 Table 元件鎖定在特定的 DOM 容器內
self.user_table = TableComponent(page.locator("#user-data-table"))
在 pytest 的 Test Case 裡呼叫時,語意會變得極度清晰且好維護:
Python
# tests/test_dashboard.py
from playwright.sync_api import Page, expect
from pages.dashboard_page import DashboardPage
def test_admin_can_search_user(page: Page):
dashboard = DashboardPage(page)
page.goto("https://admin.example.com/dashboard")
dashboard.user_table.search("Jane")
# 斷言留在 Test Case 層,向 Component 拿資料來驗證
expect(dashboard.user_table.get_rows()).to_have_count(1)
selectDateRange('2026-08-01', '2026-08-07'),而不是暴露底層下拉選單點擊 5 次的細節。data-testid 建立穩定定位.css-1x2y3z > div:nth-child(2),一樣會壞。與 Dev 約定好使用 data-testid="navbar-logout-btn",才是 POM/COM 能長期存活的基石(Day 13 會專文討論定位器設計)。掌握了 Page Object 與 Component Object 的實戰拆解後,下一個考驗我們架構能力的問題來了:
「一個可維護的測試框架,從 Test Case、Business Flow、Page/API Client 到 Test Data、Report,到底該怎麼分層排布資料夾與結構?」
明天 Day 10,我們將一起來手把手拆解:《 Day 10|測試框架的分層設計:打造清晰可讀的專案結構 》。