iT邦幫忙

2026 iThome 鐵人賽

DAY 17
1
Software Development

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

Day 17|測試資料的建立與清理策略:預建、API 產生與 Fixture 實戰

  • 分享至 

  • xImage
  •  

「好的測試資料策略就像一場完美的魔術:在測試開始時精準變出所需的場景,在測試結束後不留半點痕跡。」

大家好,我是 Jane,一個每天在第一線處理跨平台(Web & App)自動化測試、跟 CI/CD Pipeline 與測試資料纏鬥的自動化測試工程師(SDET)。

在上集 Day 16 中,我們剖析了為什麼「測試資料」比測試程式碼更容易導致自動化測試失敗。我們知道了資料污染、順序依賴與環境不一致是破壞測試可信度的三大元兇。

今天我們要來解決最硬核的落地問題:在實際的 pytest 自動化框架中,我們到底該用什麼方式來建立測試資料?測試結束後又該如何乾淨地清理?

一、 測試資料建立的 3 大策略對比

在 Web 與 App 自動化測試實務中,建立測試資料主要有以下 3 種策略:

                    ┌─────────────────────────┐
                    │  測試資料建立 3 大策略    │
                    └────────────┬────────────┘
                                 │
     ┌───────────────────────────┼───────────────────────────┐
     ▼                           ▼                           ▼
1. 預先建立 (Pre-seeded DB)     2. UI 畫面一步步建立        3. API / Factory 動態建立
   - 效能最高,但極易衝突         - 極慢且極其脆弱 (Flaky)      - 兼具速度、獨立性與彈性

| 策略 | 做法 | 優點 | 缺點 | 建議使用時機 |
| 1. 靜態預建 (Pre-seeded) | 測試前在 Staging DB 寫死數十筆固定帳號與商品。 | 測試執行速度最快,不需初始化時間。 | 多人或多 Pipeline 平行執行時極易產生資料衝突與污染。 | 全站層級的唯讀基礎資料(如:國別列表、幣別選單)。 |
| 2. 純 UI 操作建立 | 在每一個案例開頭,透過 UI 按鈕一步步點擊建資料。 | 貼近真實使用者操作。 | 速度極慢,且前置步驟只要 UI 微調就會導致整條案例失敗。 | 強烈不建議! 除非該測試目標就是在驗證「建資料流程」本身。 |
| 3. API / Factory 動態建立 | 在 Arrange 階段直接呼叫後端 API,動態建立當次案例專屬資料。 | 速度快、資料 100% 獨立隔離、不受 UI 變動影響。 | 需要維護 API Client Helper 工具。 | 黃金標準(Best Practice)! 適用於 90% 以上的測試場景。 |

SDET 黃金法則:用 API 準備前置資料(Arrange),用 UI / App 驗證核心流程(Act & Assert)!

二、 測試資料清理(Cleanup)的 4 種思維

建立資料後,如何確保不把測試環境變成垃圾場?清理資料通常有以下 4 種方式:

  1. 即時清理 (Teardown Cleanup):案例執行完畢後(無論成功或失敗),立刻在 yield 後執行 API/DB 刪除。
  2. 唯一識別碼隔離 (UUID / Unique Identifier):資料帶有唯一 ID(如 test_user_a1b2c3),即使不刪除也不會影響下一次測試(適用於無法刪除資料的系統)。
  3. 資料庫交易回滾 (DB Transaction Rollback):針對後端 API/Unit 測試,測試結束後自動 Rollback。
  4. 定期批次清理 (Nightly Cron Job):撰寫獨立清檔指令,每天深夜自動清除 DB 中帶有 test_ 前綴的垃圾資料。

三、 Python / pytest 實戰:Factory + Fixture 動態資料工廠

我們結合 Factory Pattern(工廠模式)pytest Fixture,展示一套兼具「動態生成、參數化與自動清理」的代碼架構:

1. 封裝 API Client 工具 (utils/api_client.py)

首先,封裝一個快速呼叫後端 API 建立資料的 Helper:

Python

# utils/api_client.py
import requests
from config.settings import settings

class APIClient:
    def __init__(self):
        self.base_url = settings.BASE_URL
        self.headers = {"Authorization": f"Bearer {settings.API_TOKEN}"}

    def create_user(self, user_data: dict) -> dict:
        response = requests.post(
            f"{self.base_url}/api/v1/users",
            json=user_data,
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()  # 回傳包含 user_id 的 dict

    def delete_user(self, user_id: str):
        response = requests.delete(
            f"{self.base_url}/api/v1/users/{user_id}",
            headers=self.headers
        )
        # 清理邏輯允許 404 (代表已被刪除)
        if response.status_code not in (200, 204, 404):
            print(f"[Warning] 清理使用者 {user_id} 失敗: {response.text}")

2. 設計帶有自動 Teardown 的 Data Factory Fixture (tests/conftest.py)

使用 Python 的 yield 語法,輕鬆實現 Setup > Execute > Teardown

Python

# tests/conftest.py
import uuid
import pytest
from utils.api_client import APIClient

@pytest.fixture(scope="session")
def api_client():
    return APIClient()

@pytest.fixture(scope="function")
def user_factory(api_client: APIClient):
    """
    資料工廠 Fixture:允許測試案例依需求傳參建立多個使用者,並在測試後自動清理全部建立的資料
    """
    created_user_ids = []

    def _create_user(role: str = "regular", prefix: str = "test") -> dict:
        unique_id = str(uuid.uuid4())[:8]
        user_payload = {
            "username": f"{prefix}_{role}_{unique_id}",
            "email": f"{prefix}_{unique_id}@example.com",
            "role": role,
            "password": "SecurePass123!"
        }
        user = api_client.create_user(user_payload)
        # 紀錄已建立的 ID 以供事後清理
        created_user_ids.append(user["id"])
        return user

    # 交付工廠函式給 Test Case
    yield _create_user

    # Teardown 階段:測試案例結束後(無論 Pass/Fail)自動批次清理
    for user_id in created_user_ids:
        api_client.delete_user(user_id)

3. 在 Web / App 測試案例中使用 Fixture (tests/test_checkout.py)

測試案例碼極度乾淨、語意清晰,且 100% 具備資料隔離能力:

Python

# tests/test_checkout.py
from playwright.sync_api import Page, expect

def test_vip_user_checkout_discount(page: Page, user_factory):
    """
    驗證 VIP 使用者結帳時享有折扣
    """
    # 1. Arrange: 使用工廠動態建立一名 VIP 權限的使用者
    vip_user = user_factory(role="vip", prefix="checkout_spec")

    # 2. Act: 使用當次專屬的 VIP 使用者進行 UI 登入與操作
    page.goto("https://staging.example.com/login")
    page.get_by_test_id("username-input").fill(vip_user["username"])
    page.get_by_test_id("password-input").fill("SecurePass123!")
    page.get_by_test_id("login-btn").click()

    page.goto("https://staging.example.com/checkout")

    # 3. Assert: 驗證 VIP 折扣標籤正確顯示
    expect(page.get_by_test_id("vip-discount-badge")).to_be_visible()

    # 測試結束後,conftest.py 的 user_factory 會自動呼叫 API 刪除這個 vip_user!

四、 第一線 SDET 的實務建議

  1. 將 API Client 作為自動化框架的核心基礎建設

    千萬不要覺得「我是做 UI / App 自動化的,所以我不用寫 API 呼叫」。一個優秀的 SDET,框架裡一定有強大的 API Helper,用來快速為 UI / App 測試鋪路。

  2. 清理邏輯要具備容錯能力(Graceful Cleanup)

    Teardown 階段的刪除邏輯(如 delete_user)必須加入 try-catch 或允許 404,絕對不能因為清理失敗而讓原本測試通過的案例變成紅燈

  3. 無可避免地使用預建資料時,加上『唯讀(Read-only)』約束

    對於那些建立成本極高、不得不預建在 DB 裡的資料(例如:包含幾千筆關聯資料的大型專案),限制測試案例只讀不寫。如果要修改,必須複製一份副本後再去修改副本。

明日預告

掌握了測試資料的動態建立與清理策略後,下一個考驗我們架構能力的問題來了:

「在測試過程中,系統往往會依賴外部服務(如:簡訊驗證碼、第三方金流 Gateway、信用卡授權)。我們到底該直接呼叫真實服務,還是該使用 Mock / Stub / Service Virtualization?」

明天 Day 18,我們將深入研討:《 Day 18|自動化測試如何處理外部服務:Mock、Stub 與真實整合環境的抉擇 》


上一篇
Day 16|測試資料為什麼比測試程式更難管理?剖析資料依賴與污染的陷阱
下一篇
Day 18|自動化測試如何處理外部服務:Mock、Stub 與真實整合環境的抉擇
系列文
從脆弱腳本到可信任測試平台:自動化測試架構30天23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言