iT邦幫忙

2026 iThome 鐵人賽

DAY 9
1
Software Development

從脆弱腳本到可信任測試平台:自動化測試架構30天系列 第 9

Day 9|Page Object到底該怎麼用?從濫用反思到 Component Object 實戰

  • 分享至 

  • xImage
  •  

「把所有的 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 用成了一場災難:

  • 出現幾千行的「上帝頁面類別(God Page Class)」。
  • 頁面類別裡充滿了商業邏輯斷言(Assertion)。
  • 每個小彈窗(Modal)都不知道該寫在哪個 Page 裡,最後到處複製貼上。

今天這篇文章,我們就來好好聊聊:Page Object 的真正目的是什麼?我們該如何避免濫用?以及何時該改用 Component Object Pattern?

一、 陷阱:你是不是也這樣濫用了 Page Object?

在看正確作法前,我們先來看看第一線 QA 最常踩到的 3 個 POM 濫用陷阱:

                    ┌─────────────────────────┐
                    │  Page Object 常見濫用陷阱│
                    └────────────┬────────────┘
                                 │
     ┌───────────────────────────┼───────────────────────────┐
     ▼                           ▼                           ▼
1. 演變成上帝物件 (God Object)  2. 職責混淆 (Assertion Leak)  3. 鏈式呼叫地獄 (Method Chaining)

陷阱 1:上帝物件(God Page Class)

後台管理系統(Dashboard)或電商首頁,頁面上包含導覽列、搜尋列、商品列表、輪播 Banner、側邊欄、頁尾……如果全部寫在 HomePage.ts 裡,這個檔案很快就會膨脹到 2000 多行。維護它比維護測試腳本還痛苦!

陷阱 2:在 Page Object 裡面寫斷言(Assertion Leak)

expect(element).toBeVisible()assert title == "..." 寫在 Page Object 的方法內。

  • 為什麼不好:Page Object 的職責是「提供頁面的服務與操作能力」,而不是「驗證測試結果」。一旦斷言綁死在 Page 裡面,這個 Page 就失去了在不同測試情境下的複用性。

陷阱 3:過度使用 Method Chaining(鏈式呼叫)

為了讓測試案例寫起來像 page.goto().fillUser().clickSubmit().expectSuccess(),強迫每個 Page 方法都要回傳下一個頁面的 PageObject

  • 為什麼不好:現代 Single Page Application (SPA) 的流轉非常動態(可能輸入錯密碼留在原頁、輸入對密碼轉跳、或跳出二次驗證 Modal)。在 Page Object 底層硬綁轉跳邏輯,只會讓彈性變得很差。

二、 真正的職責分離:測試操作層 vs. 業務流程層

一個健康的 Web UI 自動化架構,應該分成三個層級:

┌─────────────────────────────────────────────────────────────┐
│ 1. Test Case 層 (測試意圖與斷言)                             │
│    - 負責:Arrange - Act - Assert、商業期待斷言              │
├─────────────────────────────────────────────────────────────┤
│ 2. Business Flow / Service 層 (業務流程組合 - 選填)          │
│    - 負責:將多個 UI 步驟組合為商業服務 (如: completeCheckout)│
├─────────────────────────────────────────────────────────────┤
│ 3. Page Object / Component Object 層 (UI 操作與元素封裝)     │
│    - 負責:Locator 管理、元素基本操作 (click, fill, get)      │
└─────────────────────────────────────────────────────────────┘

黄金法則:

  1. Page Object 內「絕對不寫 expect()assert(除非是檢查頁面是否已載入完成的內部狀態判斷)。
  2. Test Case 層「絕對不出現 DOM 選擇器或 Locator」(如不寫 page.click('.btn-submit'))。

三、 進階解法:引入 Component Object Model (COM)

為了解決大型頁面導致 Page Object 肥大的問題,現代 Web UI 自動化更推崇 Component Object Model(元件物件模式)

Web 發展到 React / Vue / Angular 後,前端早已不是以「頁面(Page)」為單位,而是以「元件(Component)」為單位。測試架構也應該跟進!

什麼是 Component Object?

將頁面上重複出現或獨立的區塊,抽離成獨立的 Component 物件,例如:

  • NavbarComponent(頂部導覽列)
  • ModalComponent(通用對話框/彈窗)
  • TableComponent(資料表格與分頁)
  • CartDrawerComponent(側滑購物車)

程式碼範例對比 (Python + Playwright):

❌ 傳統肥大的 Page Object:

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()

⭕ 使用 Component Object 解耦與組合:

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)

四、 第一線 Web QA 的實戰心法

  1. 從 DOM 樹結構思考,而不是從整個畫面思考
    看到一個新頁面時,先劃分區塊:哪些是全站通用的 Component(如 Header/Footer)?哪些是這個頁面獨有的區塊?將通用的元件抽離,可以少寫 50% 的定位器程式碼。
  2. 封裝「元件語意」,而不是封裝「單純的 click」
    Component Object 提供的方法應該要有商業語意,例如 selectDateRange('2026-08-01', '2026-08-07'),而不是暴露底層下拉選單點擊 5 次的細節。
  3. 配合 data-testid 建立穩定定位
    Component Object 再漂亮,如果底層抓的是脆弱的 .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|測試框架的分層設計:打造清晰可讀的專案結構 》


上一篇
Day 8|測試程式也需要良好的程式碼品質:別讓你的測試程式碼變成技術債
系列文
從脆弱腳本到可信任測試平台:自動化測試架構30天9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言