Mock 與隔離外部依賴
Mock 是在測試中用來「假扮」真實物件的替身:當程式依賴網路 API、資料庫、目前時間或隨機數時,用 mock 取代這些外部依賴,測試就能快速、穩定、可重複地執行,還能檢查程式是否用正確的參數呼叫了它們。pytest 本身不提供 mock,通常搭配 Python 標準函式庫的 unittest.mock,或對它做了 pytest 包裝的 pytest-mock 外掛。
什麼時候需要 mock?
| 依賴 | 不 mock 會怎樣 |
|---|---|
| 網路 API | 慢、會因網路或對方服務異常而失敗、可能產生費用 |
| 寄信、簡訊、金流 | 測試會真的寄出去或扣款 |
目前時間 datetime.now() |
結果每次不同,無法寫固定的預期值 |
| 隨機數 | 同上 |
反過來說,自己寫的純邏輯不需要 mock。mock 太多會讓測試只是在驗證「mock 的設定」,而不是程式真正的行為。
Mock 物件的基本用法
Mock 物件可以接受任何呼叫,並記錄被怎麼呼叫:
from unittest.mock import Mock
sender = Mock()
sender.send("amy@example.com", "歡迎加入")
sender.send.assert_called_once_with("amy@example.com", "歡迎加入")
設定回傳值或讓它拋出例外:
用 patch 替換模組裡的物件
假設有一個查匯率的模組:
import requests
def get_rate(currency):
resp = requests.get(f"https://api.example.com/rates/{currency}")
resp.raise_for_status()
return resp.json()["rate"]
def to_twd(amount, currency):
return round(amount * get_rate(currency), 0)
測試 to_twd 時,把 get_rate 換掉,就不會真的連網:
from unittest.mock import patch
from my_package import exchange
def test_to_twd():
with patch("my_package.exchange.get_rate", return_value=32.5) as mock_rate:
assert exchange.to_twd(100, "USD") == 3250
mock_rate.assert_called_once_with("USD")
patch 的路徑該寫哪裡?
規則是:patch「被使用的地方」,而不是「被定義的地方」。如果 report.py 寫的是 from my_package.exchange import get_rate,report 模組裡已經有自己的 get_rate 名稱,這時要 patch my_package.report.get_rate;patch my_package.exchange.get_rate 不會影響 report。這是 mock 最常見的錯誤。
pytest-mock:更 pytest 風格的寫法
pytest-mock 提供 mocker fixture,用法與 patch 相同,但不需要 with 區塊,測試結束時自動還原:
from my_package import exchange
def test_to_twd(mocker):
mock_rate = mocker.patch("my_package.exchange.get_rate", return_value=32.5)
assert exchange.to_twd(100, "USD") == 3250
mock_rate.assert_called_once_with("USD")
用 autospec 防止寫錯
一般 Mock 接受任何方法名稱與參數,就算你打錯字也不會報錯,測試照樣通過。加上 autospec=True,mock 會照真實物件的簽名檢查呼叫方式:
monkeypatch、mock 該選哪個?
- 只需要換掉某個值或函式:用內建的
monkeypatch,簡單、不需額外套件。 - 需要驗證呼叫(呼叫幾次、帶什麼參數)或模擬例外:用
unittest.mock或 pytest-mock。 - 更好的長期做法是在設計上讓依賴可以從外部傳入(依賴注入),測試時直接傳入假物件,就不需要 patch 任何路徑,詳見 寫出好測試。
推薦影音
How to Write Great Unit Tests in Python(ArjanCodes)
ArjanCodes 以一個天氣查詢服務(WeatherService)為例,從基本測試原則一路示範 monkeypatch、mock、fixture 與參數化,說明如何把依賴外部 API 的程式寫成好測試的樣子,約 23 分鐘。
Professional Python Testing with Mocks(NeuralNine)
NeuralNine 約 21 分鐘的 mock 專題,示範如何用 mock 取代外部依賴來測試 Python 程式,適合想多看幾個 mock 實例的讀者。