「寫出一支能跑通的測試腳本只需要半天;但要設計一套能讓團隊維護三年的測試框架,你需要考慮的是整個架構。」
大家好,我是 Jane。
在前 5 天,我們聊完了自動化測試的觀念、策略與價值衡量指標。從今天開始,我們要正式進入技術實務層面:如何從頭打造一套「高穩定度、低維護成本」的測試框架?
許多剛接觸自動化的 QA 常問我一個問題:
.spec.ts 檔,裡面能自動打開瀏覽器、輸入帳號密碼、點擊登入、最後驗證頁面文字。這算不算是『測試框架』?」我的答案通常是:「這叫『測試腳本(Test Script)』,還不叫『測試框架(Test Framework)』。」
當你的專案只有 3 個案例時,寫單一腳本非常快、非常直覺;但當案例膨脹到 50 個、100 個,且需要整合到 CI/CD 時,沒有框架支撐的腳本將會變成一場維護災難。今天這篇文章,我們就來聊聊這兩者到底差在哪裡?
我們用一個簡單的對比來看兩者的本質差異:
| 比較維度 | 測試腳本 (Test Script) | 測試框架 (Test Framework) |
|---|---|---|
| 關注焦點 | 「如何完成某個特定的操作與驗證」 | 「如何組織、執行、管理所有測試與資源」 |
| 代碼結構 | 線性順序(Linear),將環境設定、資料、步驟、驗證全塞在一起 | 分層架構(Layered),職責分離(Separation of Concerns) |
| 資料處理 | 測試資料寫死(Hardcoded)在程式碼中 | 資料與邏輯分離,支援 Fixtures / Factory / 多環境配置 |
| 維護成本 | 頁面改版或流程變動時,需要逐一修改幾十個腳本 | 只需要修改單一層級或元件(如 Page Object 或 API Client) |
| 規模化能力 | 僅適用於少數案例(< 10 個) | 可支援數百上千個案例,並支援平行執行與 CI 整合 |
簡單來說:腳本是「劇本裡的一幕」,而框架是「整座劇院的運作系統」。
如果把寫好的 10 支單一腳本重構成一套「可維護的測試框架」,我們至少需要抽離並處理以下 7 大核心要素:
┌─────────────────────────┐
│ 自動化測試框架 7 大核心 │
└────────────┬────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
1. 測試結構與生命週期 3. 測試資料管理 (Data) 5. 統一斷言機制 (Assertion)
(Structure & Hooks) 4. 共用操作封裝 (Flows) 6. 報告與 Log (Reporting)
2. 環境與設定 (Config) 7. 錯誤與例外處理 (Error)
BeforeAll / BeforeEach / AfterEach / AfterAll 等 Hook 機制,將測試的前置準備(Setup)與事後清理(Teardown)標準化。| 傳統 Hook 概念 | Pytest Fixture 實現方式 | 說明 |
|---|---|---|
| BeforeAll | @pytest.fixture(scope="session") 中 yield 之前 的程式碼 |
在所有測試開始前僅執行一次 |
| BeforeEach | @pytest.fixture(scope="function") 中 yield 之前 的程式碼 |
在每個測試函式執行前執行一次 |
| AfterEach | @pytest.fixture(scope="function") 中 yield 之後 的程式碼 |
在每個測試函式執行後執行一次 |
| AfterAll | @pytest.fixture(scope="session") 中 yield 之後 的程式碼 |
(或 conftest.py 中的 pytest_sessionfinish) | 在所有測試結束後僅執行一次 |
conftest.py 與 @pytest.fixture底下展示如何在 Pytest 中完整實現這四個階段的初始化與清理工作。
conftest.py(測試設定檔)Python
import pytest
# ==========================================
# 1. Session 級別(對應 BeforeAll & AfterAll)
# ==========================================
@pytest.fixture(scope="session", autouse=True)
def db_connection():
# --- BeforeAll 階段 ---
print("\n[BeforeAll] 建立全域資料庫連線...")
connection = {"status": "connected"}
yield connection # 將資源提供給測試使用
# --- AfterAll 階段 ---
print("\n[AfterAll] 關閉全域資料庫連線...")
connection["status"] = "disconnected"
# ==========================================
# 2. Function 級別(對應 BeforeEach & AfterEach)
# ==========================================
@pytest.fixture(scope="function")
def user_data():
# --- BeforeEach 階段 ---
print("\n [BeforeEach] 為當前測試重置/準備乾淨的使用者資料")
data = {"user_id": 1, "username": "alice"}
yield data # 傳遞資料給測試函式
# --- AfterEach 階段 ---
print("\n [AfterEach] 清理當前測試產生的臨時資料與狀態")
data.clear()
test_user.py(測試檔案)Python
def test_user_login(user_data):
print(" -> 正在執行 test_user_login")
assert user_data["username"] == "alice"
def test_user_update(user_data):
print(" -> 正在執行 test_user_update")
user_data["username"] = "bob"
assert user_data["username"] == "bob"
執行 pytest -s test_user.py(-s 參數用於顯示 print 輸出):
Plaintext
[BeforeAll] 建立全域資料庫連線...
[BeforeEach] 為當前測試重置/準備乾淨的使用者資料
-> 正在執行 test_user_login
[AfterEach] 清理當前測試產生的臨時資料與狀態
.
[BeforeEach] 為當前測試重置/準備乾淨的使用者資料
-> 正在執行 test_user_update
[AfterEach] 清理當前測試產生的臨時資料與狀態
.
[AfterAll] 關閉全域資料庫連線...
pytest_sessionfinish)如果你真的需要直接監聽 Pytest 內部的系統 Hook(而不是透過 Fixture),可以在 conftest.py 中使用內建 Hook 函式:
Python
# conftest.py
def pytest_sessionstart(session):
"""最底層的 BeforeAll:pytest 會話啟動時觸發"""
print("\n>>> Pytest Session 開始啟動")
def pytest_sessionfinish(session, exitstatus):
"""最底層的 AfterAll:pytest 會話完全結束時觸發"""
print("\n>>> Pytest Session 結束,離開狀態碼:", exitstatus)
結語:在 Python / pytest 世界中,90% 以上的需求使用 @pytest.fixture(搭配 yield 和對應的 scope)即可優雅地處理生命週期 Hook!
https://staging.example.com,換到 Test/Dev 環境就要改 Code。.env 或 config.json),支援透過命令列切換環境。test_user@gmail.com 直接 Hardcode 在程式碼內。loginService.loginAsAdmin())或採用 Page Object Pattern(Day 9 會詳細討論),讓腳本只表達「測試意圖」,不寫底層操作細節。assert title == "Home",失敗時只印出 AssertionError。console.log() 或 Terminal 輸出,跑完就沒了。看到這裡,你可能會想:「天啊,要做一套框架竟然要處理這麼多事?那我是不是要花兩個月把架構寫完美才能開始寫測試?」
千萬不要! 這是很多程式底子好的 QA 最常踩的陷阱:過度抽離與抽象化(Over-engineering)。
最健康的演進路線是「漸進式重構(Progressive Refactoring)」:
好的測試框架是「長出來的」,而不是「一口氣畫出來的」。
了解了框架需要處理的 7 大要素後,我們接下來要深入到「單一測試案例」的內部品質。
許多人在寫腳本時,喜歡把「登入 > 搜尋商品 > 加購物車 > 填寫地址 > 付款 > 驗證訂單」全寫在同一條測試案例裡,結果一旦失敗,完全不知道是卡在哪一步。
明天 Day 7,我們就來聊聊:《 Day 7|一個測試案例應該負責多少事情?論測試案例的粒度與獨立性 》。