- 0. 前置觀念:什麼時候需要 Mock?
- 在單元測試(Unit Test)中,我們的黃金規則是:測試應該是快速、獨立且穩定的。
- 但是,當你的程式碼包含以下依賴時,這些規則會被打破:
- 呼叫外部 API (例如:金流服務、天氣 API)→會慢,且需要網路。
- 資料庫操作→會弄髒資料,且設定麻煩。
- 隨機或時間相關 (例如:
datetime.now()
)→每次結果都不一樣,無法斷言。
- 這時,我們就需要 Mock(模擬)。我們要用一個「假的物件」來替換掉那個「難搞的外部依賴」,讓測試能專注驗證你的商業邏輯。
- 1. 環境準備
- 請確保安裝了以下套件:
- pip install pytest pytest-mock
- 2. 案例場景 (Production Code)
- 為了讓大家理解邏輯,我們模擬一個 「電商支付系統」。
- 外部依賴:
BankApi
(它負責真的去扣款,我們不想真的執行它)。
- 被測系統 (SUT):
PaymentService
(這是我們寫的邏輯,我們要測試它)。
- 建立一個檔案
src.py
:
- 3. 測試程式邏輯 (Test Code)
- 現在我們要測試
process_payment
,但我們要切斷與 BankApi
的真實連結。
- 建立測試檔案
test_payment.py
。
- 核心邏輯流程 (3A 原則)
- 在使用
mocker
時,你的腦袋要跟著這三個步驟走:
- Arrange (佈局/設定):
- 使用
mocker.patch
鎖定要替換的物件。
- 告訴這個 Mock 物件:「等一下如果有人叫你,你就回傳這個值 (Return Value)」。
- Act (執行):
- Assert (驗證):
- 驗證結果是否符合預期。
- 驗證 Mock 物件是否真的被呼叫了 (確保你的程式碼有走到那一行)。
- 實作教學
# test_payment.py
import pytest
from src import PaymentService, BankApi
# 測試案例 1: 模擬銀行扣款成功
def test_payment_success(mocker):
# --- 1. Arrange (佈局) ---
# 步驟 A: 建立 Mock 物件
# 我們不需要真的 new 一個 BankApi,我們用 mocker 產生一個假的
# 注意:雖然這裡我直接傳 mock 物件進去,但更常見的是 patch 某個類別的方法
mock_bank = mocker.Mock(spec=BankApi)
# 步驟 B: 設定劇本 (Stubbing)
# 翻譯:當有人呼叫 mock_bank.charge 時,不要真的去扣款,直接回傳 True
mock_bank.charge.return_value = True
# 初始化被測系統,注入假的依賴
service = PaymentService(bank_api=mock_bank)
# --- 2. Act (執行) ---
result = service.process_payment("1234-5678", 100)
# --- 3. Assert (驗證) ---
# 驗證回傳結果 (State Verification)
assert result == "Payment Successful"
# 驗證行為 (Behavior Verification) - 這一步是 Mock 的精髓
# 我們要確認:Service 真的有拿正確的參數去呼叫 Bank 嗎?
mock_bank.charge.assert_called_once_with("1234-5678", 100)
# 測試案例 2: 模擬銀行連線發生異常 (Side Effect)
def test_payment_exception(mocker):
# --- Arrange ---
mock_bank = mocker.Mock(spec=BankApi)
# 設定劇本:這次不回傳值,而是「拋出例外」
# side_effect 用來模擬連線中斷、Timeout 等錯誤情況
mock_bank.charge.side_effect = TimeoutError("Bank timeout")
service = PaymentService(bank_api=mock_bank)
# --- Act ---
result = service.process_payment("1234-5678", 100)
# --- Assert ---
# 確認我們的程式碼有 catch 住例外並回傳正確的錯誤訊息
assert result == "Transaction Error"
- 4. 總結:Mock 測試的思維邏輯
- 作為一個測試工程師,當你寫測試遇到困難(例如無法連線、不想等待)時,請依照以下邏輯思考:
- 識別依賴:是哪一行程式碼在阻礙測試?(例如:
api.charge()
)
- 建立替身:使用
mocker
創造一個假物件。
- 編寫劇本:使用
.return_value
(模擬正常回傳) 或 .side_effect
(模擬報錯)。
- 執行並驗證:不僅驗證結果正確,還要驗證替身是否有被正確呼叫 (
assert_called_with
)。
- 常用的 pytest-mock 指令速查
mocker.patch(‘path.to.target’)
: 替換指定路徑的物件。
mock_obj.return_value = X
: 指定回傳值。
mock_obj.side_effect = Exception()
: 指定拋出錯誤。
mock_obj.side_effect = [A, B, C]
: 第一次呼叫回傳 A,第二次 B…
mock_obj.assert_called_once()
: 驗證只被呼叫一次。
Post Views: 73