Pytest(2) Mock 實戰教學:掌握測試替身的核心邏輯

  • 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 (執行)
        • 執行你的 PaymentService
      • 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(): 驗證只被呼叫一次。
0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments