本文 AI 產出,尚未審核

Python 單元測試與除錯:深入了解 mock 模組


簡介

在開發 Python 應用程式時,單元測試是保證程式正確性與可維護性的根本工具。
然而,許多程式碼會與外部資源(例如資料庫、網路服務、檔案系統)或複雜的物件互動,直接對這些依賴進行測試往往既慢又不穩定。

這時 mock(模擬)技術就顯得格外重要。Python 標準庫中的 unittest.mock(在 Python 3.3 之後直接內建)允許我們 「偽造」 任何函式、類別或屬性,讓測試僅聚焦於被測試單元本身的行為,而不必真的呼叫外部服務。

本文將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹如何在 Python 測試中活用 mock 模組,幫助初學者快速上手,也讓中階開發者提升測試品質與除錯效率。


核心概念

1. 為什麼需要 Mock?

  • 隔離測試:將測試對象(System Under Test, SUT)與其依賴分離,避免外部因素干擾測試結果。
  • 提升效能:不必真的連線資料庫或呼叫第三方 API,測試執行速度可提升數十倍。
  • 可預測的回傳:使用 mock 可以自行定義回傳值或拋出例外,方便驗證錯誤處理邏輯。

簡單來說,mock 就是「假裝」某個物件真的存在,卻讓我們自行決定它的行為。

2. unittest.mock 的主要工具

工具 說明
Mock 最基礎的偽造物件,可自行設定屬性、回傳值與呼叫次數驗證。
MagicMock 繼承自 Mock,自動為大部分「魔術方法」 (__len____getitem__ 等) 提供 mock 行為。
patch 裝飾器或上下文管理器,用於 暫時取代 指定模組或類別的實作。
patch.object 只針對已載入的物件(例如類別或實例)進行替換。
call 用於驗證 mock 被呼叫的參數與次序。

3. 使用 patch 的原理

patch 會在測試執行期間 把目標名稱指向一個新的 Mock 物件,測試結束後自動還原。
重要的是 要 patch 目標的「引用路徑」,而不是原始定義的路徑。

例如,若 module_a.pyfrom module_b import fetch_data,在測試 module_a 時必須 patch module_a.fetch_data,而不是 module_b.fetch_data,因為 module_a 已經把 fetch_data 直接引用進自己的命名空間。


程式碼範例

以下示範 5 個常見且實用的 mock 用法,均以 Python 3 為例,程式碼區塊使用 python 標記。

範例 1:最簡單的 Mock 物件

from unittest.mock import Mock

# 建立一個 Mock,預設回傳值為 None
dummy = Mock()
print(dummy())          # => None

# 設定回傳值
dummy.return_value = 42
print(dummy())          # => 42

# 驗證呼叫次數
dummy()
dummy(1, 2, key='value')
print(dummy.call_count)        # => 2
print(dummy.call_args)         # => call(1, 2, key='value')

重點Mock 會自動記錄每一次的呼叫資訊,方便在測試結束後斷言 (assert)。

範例 2:使用 patch 替換函式

假設有一個簡單的 HTTP 客戶端 api.py

# api.py
import requests

def get_user(user_id):
    resp = requests.get(f"https://example.com/users/{user_id}")
    resp.raise_for_status()
    return resp.json()

在測試 get_user 時,我們不想真的發出網路請求:

# test_api.py
import unittest
from unittest.mock import patch
from api import get_user

class TestGetUser(unittest.TestCase):

    @patch('api.requests.get')      # patch 目標是 api 模組內的 requests.get
    def test_success(self, mock_get):
        # 設定 mock 的回傳值
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {'id': 1, 'name': 'Alice'}

        result = get_user(1)
        self.assertEqual(result['name'], 'Alice')
        mock_get.assert_called_once_with('https://example.com/users/1')

技巧mock_get.return_value.json.return_value 直接鏈式設定 json() 方法的回傳結果。

範例 3:使用 MagicMock 模擬容器行為

from unittest.mock import MagicMock

# 假設有一個函式會遍歷傳入的序列
def process_items(seq):
    total = 0
    for item in seq:
        total += item
    return total

# 用 MagicMock 來偽造一個支援迭代的物件
mock_seq = MagicMock()
mock_seq.__iter__.return_value = iter([1, 2, 3, 4])

print(process_items(mock_seq))   # => 10

MagicMock 自動提供 __iter____len____getitem__ 等魔術方法,使其可以像真實容器一樣被使用。

範例 4:patch.object 直接替換類別方法

# db.py
class Database:
    def connect(self):
        # 真實環境會連線資料庫
        ...

    def fetch(self, sql):
        # 執行查詢
        ...

# test_db.py
import unittest
from unittest.mock import patch, MagicMock
from db import Database

class TestDatabase(unittest.TestCase):
    def test_fetch(self):
        db = Database()
        # 只 patch db 實例的 fetch 方法
        with patch.object(db, 'fetch', return_value=[{'id': 1}]) as mock_fetch:
            result = db.fetch('SELECT * FROM users')
            self.assertEqual(result, [{'id': 1}])
            mock_fetch.assert_called_once_with('SELECT * FROM users')

patch.object 讓我們 只針對單一實例 進行 mock,避免影響其他同類別的物件。

範例 5:驗證多次呼叫的參數順序(call

from unittest.mock import Mock, call

def workflow(service):
    service.start()
    service.process('step1')
    service.process('step2')
    service.finish()

mock_service = Mock()
workflow(mock_service)

expected_calls = [
    call.start(),
    call.process('step1'),
    call.process('step2'),
    call.finish()
]
assert mock_service.mock_calls == expected_calls

mock_calls 會以 呼叫順序 收集所有方法與參數,使用 call 物件可以直觀比對。


常見陷阱與最佳實踐

陷阱 說明 解決方式
Patch 錯誤路徑 把目標 patch 成錯誤的模組路徑,導致測試仍然呼叫真實實作。 先確認 引用位置(使用 print(module.__file__)inspect.getsource)再 patch。
忘記還原 手動替換屬性後未還原,會影響其他測試。 使用 with patch(...): 或裝飾器,自動還原。
過度 Mock 把太多的依賴都 mock,測試失去意義,等於測試 mock 本身。 只 mock 外部或成本高 的依賴,保留核心邏輯的真實執行。
Mock 物件的預設行為是 MagicMock 有時忘記使用 return_value,導致呼叫 .json() 時回傳另一個 Mock。 明確設定 mock_obj.method.return_value,或使用 side_effect
Side Effect 與 Return Value 同時使用 兩者同時設定會產生衝突,導致測試不預期。 只使用其中一種,或在 side_effect 中自行處理回傳值。

最佳實踐

  1. 遵循「Arrange-Act-Assert」:先安排(Arrange)mock 的行為與狀態,接著執行(Act)被測試函式,最後斷言(Assert)呼叫與結果。

  2. 使用 autospec=True:在 patch 時加入 autospec=True,可以自動產生與原始函式相同的簽名,避免傳入錯誤參數。

    @patch('module.func', autospec=True)
    def test_func(mock_func):
        mock_func.return_value = 123
        ...
    
  3. 盡量使用 assert_called_once_with:比手動檢查 call_count 更直觀。

  4. 分層測試:單元測試只 mock 直接依賴,整合測試(integration test)則使用真實服務,確保系統整體運作。

  5. 文件化 Mock 行為:在測試程式碼中加入註解,說明為何要 mock、預期的回傳與例外情形,提升可讀性與維護性。


實際應用場景

場景 目的 Mock 方式
呼叫第三方 REST API 測試 API 客戶端的錯誤處理與資料解析。 patch('package.requests.get'),設定 status_codejson() 回傳。
資料庫存取 測試 ORM 模型的商業邏輯,避免真的寫入資料庫。 patch('module.Session'),使用 MagicMock 模擬 query()add()commit()
檔案 I/O 測試檔案讀寫流程,如 CSV 解析或設定檔載入。 patch('builtins.open', mock_open(read_data='a,b\\n1,2'))
長時間運算或外部程式呼叫 測試程式在子程序失敗時的回復機制。 patch('subprocess.run', side_effect=CalledProcessError(...))
時間與隨機性 測試依賴 datetime.now()random.random() 的程式。 patch('module.datetime')patch('module.random.random'),設定固定回傳值。

這些情境下,使用 mock 能讓測試快速、可靠且可重複,同時讓開發者在除錯時能清楚看到每一次依賴被呼叫的參數與次數。


總結

  • unittest.mock 是 Python 標準庫中最強大的測試輔助工具,能 偽造函式、類別與屬性,讓單元測試聚焦於核心邏輯。
  • 正確的 patch 路徑、適度的 Mock 範圍、以及 斷言呼叫 的技巧,是寫出可靠測試的關鍵。
  • 常見的陷阱(錯誤路徑、過度 Mock、忘記還原)只要遵循最佳實踐(with patchautospec、分層測試)即可輕鬆避免。
  • 在實務開發中,從 API 客戶端、資料庫操作、檔案 I/O、子程序呼叫時間與隨機性,幾乎所有與外部環境交互的程式碼,都可以透過 mock 進行快速、可預測的測試。

掌握了 mock 的使用方法,你不僅能寫出 更快、更穩定 的測試套件,也能在除錯時更快定位問題根源,提升整體開發效能。祝你在 Python 測試的旅程中玩得開心、寫得順利!