「如果你的自動化測試因為第三方金流 API 伺服器掛掉、或是簡訊驗證碼發送次數超限而跳紅燈,那失敗的不是你的產品,而是你的測試隔離策略。」
大家好,我是 Jane,一個每天在第一線處理跨平台(Web & App)自動化測試、跟 CI/CD Pipeline 與各種外部依賴服務奮戰的自動化測試工程師(SDET)。
在上集 Day 16 與 Day 17 中,我們解決了測試資料的動態建立與清理問題。
然而在現代的 Web 與 App 系統中,我們的產品很少是孤立運作的。系統內部常充斥著各種外部服務的串接:
當我們在執行自動化測試時,這些外部服務常常成為最大的 Flaky 來源 與 執行瓶頸。
今天這篇文章,我們就來好好聊聊:自動化測試該如何處理解外依賴?Mock、Stub、Fake 到底有什麼不同?我們又該如何在『真實呼叫』與『模擬隔離』之間做抉擇?
在討論隔離策略前,我們先用一張圖與簡單比喻搞懂這三個常被混淆的名詞:
┌─────────────────────────┐
│ 外部服務模擬 3 兄弟 │
└────────────┬────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
1. Stub (罐頭回應) 2. Mock (行為驗證) 3. Fake (簡化版實作)
- 回傳固定的 JSON/狀態碼 - 驗證 API 是否有被正確呼叫 - 記憶體內的簡易資料庫/服務
/pay,Stub 就直接固定回傳 {"status": "SUCCESS", "transaction_id": "tx_123"}。100。身為 SDET,我們不能無腦地把所有外部 API 全部 Mock 掉,否則測試會失去真實性;但也不能全部呼叫真 API,否則測試會變得極慢且不可靠。
┌────────────────────────────────────────────────────────────────────────┐
│ 外部服務處置抉擇矩陣 │
├────────────────────────────────────────────────────────────────────────┤
│ 1. 每次呼叫會產生真實費用 (如: 簡訊、AI API, Google Maps) ──▶ 必須 Mock │
│ 2. 存在嚴格的 Rate Limit (如: 發送驗證碼限制 5 次/分) ──▶ 必須 Mock │
│ 3. 無法輕易在測試環境觸發的情境 (如: 銀行扣款失敗 500) ──▶ 必須 Mock │
│ 4. 產品核心商業串接 (如: Smoke Test 驗證真實金流 Sandbox) ──▶ 真實呼叫 │
└─────────────────────────────────────────────────────────────────────────┘
在 Web 與 App 自動化測試中,我們主要可以在 後端服務層 或 前端/網路層 進行 Mock。
如果你的 Web 或 App Webview 在前端會直接發送 HTTP 請求給第三方 API,我們可以使用 Playwright 的 page.route() 在瀏覽器網路層直接攔截,不需要修改任何後端 Code!
Python
# tests/test_payment_mock.py
from playwright.sync_api import Page, expect
def test_checkout_handles_payment_failure_gracefully(page: Page):
"""
測試情境:模擬第三方金流 API 回傳 500 錯誤時,前端頁面是否會正確顯示友善提示
"""
# 1. 使用 route() 攔截第三方金流 API 請求,並 Mock 回傳 500 錯誤
def handle_payment_api(route):
route.fulfill(
status=500,
content_type="application/json",
body='{"code": "PAYMENT_FAILED", "message": "Bank server busy"}'
)
# 攔截包含特定 URL pattern 的 API 呼叫
page.route("**/api/third-party/payment", handle_payment_api)
# 2. 執行正常的 UI 操作
page.goto("https://staging.example.com/checkout")
page.get_by_test_id("pay-btn").click()
# 3. Assert: 驗證前端成功捕捉 500 錯誤,並顯示「付款失敗,請稍後再試」
expect(page.get_by_test_id("error-banner")).to_be_visible()
expect(page.get_by_test_id("error-banner")).to_have_text("付款失敗,請稍後再試")
unittest.mock / pytest-mock(適合後端 API 測試)如果外部服務是由你的後端伺服器(Python/FastAPI/Django)在背景呼叫,我們可以在 Python 測試中使用 mocker 替換掉 API 呼叫類別:
Python
# tests/test_order_service.py
import pytest
from services.order_service import OrderService
def test_process_order_success(mocker):
"""
測試情境:驗證訂單服務在第三方簡訊發送成功時,能正常完成訂單流程
"""
# 1. 使用 pytest-mock 攔截 SMSClient 的 send_sms 方法,強制回傳 True
mock_sms = mocker.patch("utils.sms_client.SMSClient.send_sms", return_value=True)
order_service = OrderService()
# 2. 執行訂單處理邏輯
result = order_service.process_order(order_id="ORD_12345")
# 3. Assert 1: 驗證商業邏輯回傳成功
assert result["status"] == "COMPLETED"
# 4. Assert 2 (Mock 行為驗證): 驗證 SMSClient 確實有被呼叫了 1 次
mock_sms.assert_called_once_with(phone="0912345678", message=mocker.ANY)
討論完外部服務與 Mock 的抉擇後,接下來我們要比較 UI 測試與 API 測試在價值上的根本差異:
「為什麼在自動化測試的世界裡,API 測試通常比 UI 測試划算 10 倍?我們該如何安排這兩者的比例?」
明天 Day 19,我們將一起來剖析:《 Day 19|為什麼 API 測試通常比 UI 測試更划算?效益與維護成本深度剖析 》。