深入解析 monkeypatch:Python 測試中的強大工具
更新日期: 2025 年 2 月 13 日
本文為 AI 描述優化 api 設計 系列文,第 11 篇:
- 如何設計一個商品描述優化 API?—— 完整指南
- 設計 AI 優化商品描述的 Prompt
- FastAPI:高效且易用的 Python Web 框架
- 介紹 Uvicorn:高效能 ASGI 伺服器
- Uvicorn 監聽 0.0.0.0,但為何 API 只能用 127.0.0.1 訪問?
- Tenacity:強大的 Python 重試機制庫
- FastAPI 建立商品描述優化 API
- 介紹 pytest:Python 測試框架的強大選擇
- Python httpx 完整指南:高效的 HTTP 客戶端
- Pytest-Benchmark:高效能測試的基準分析工具
- 深入解析 monkeypatch:Python 測試中的強大工具 👈進度
- API 自動化測試與效能優化
建議閱讀本文前,先具備 聊天機器人-建議回復 相關系列文的概念。
在撰寫測試時,我們經常會遇到一些問題,例如:
- 需要測試的函數會調用外部 API,但我們不希望真正發送請求。
- 某些函數會讀取環境變數,而我們希望在測試時提供不同的值。
- 某些系統函數可能會影響測試結果,導致測試不穩定。
這時候,pytest
提供的 monkeypatch
工具就能派上用場。
它允許我們在測試期間動態修改函數、類別、屬性、環境變數等,使我們能夠更方便地控制測試環境,確保測試結果的可預測性與穩定性。
本文將詳細介紹 monkeypatch
的用途、使用方式及最佳實踐,讓你能夠在 Python 測試中得心應手地運用它。
命名由來
monkeypatch
這個名稱的由來與 “Monkey Patching”(猴子補丁) 這個概念有關,它是一種在 執行時動態修改程式行為 的技術。
- Monkey(猴子)+ Patch(補丁)
“Monkey” 在程式設計領域中有時代表 不受控制的修改或即興的變更,而 “Patch” 則表示對程式的修補。因此,monkeypatch
這個詞的意思就是 “像猴子一樣隨意地修改程式碼”。 - 靈感來自猴子的行為
猴子(monkey)以頑皮和隨機的行為著稱,而 “Monkey Patching” 這個詞則形象地描述了一種在程式執行時對既有程式碼進行動態修改的方式,就像猴子亂動某些東西一樣,使它產生不同的行為。 - 起源於 Ruby 和 Smalltalk
- 這個術語最早可以追溯到 Smalltalk,後來在 Ruby 社群中廣為流傳,指的是在不修改原始碼的情況下,動態覆蓋或替換現有的方法。
- 例如,在 Ruby 中可以重新定義核心函數,讓它表現出不同的行為。
- Python 社群的採用
- Python 也支援這種動態修改技術,例如透過
monkeypatch
來臨時替換函數、類別或屬性,在測試時模擬不同的場景。 pytest
的monkeypatch
工具就是基於這種概念,專門用來 讓測試可以控制某些行為,而不影響程式的永久運行。
- Python 也支援這種動態修改技術,例如透過
monkeypatch
是什麼?
monkeypatch
是 pytest
內建的一個 fixture,允許我們在測試時暫時修改函數或屬性,並在測試結束後自動還原原始行為。
它的核心目標是 提供一種安全且可控的方式來修改程式行為,讓測試能夠模擬各種場景,而不影響實際程式的運行。
monkeypatch
主要用於:
- 替換函數或方法(Mock 內建函數或第三方函數)
- 修改類別或屬性
- 變更環境變數
- 移除屬性或方法
補充:什麼是 fixture
?
在 pytest
中,fixture
是一種提供測試前置(setup)和後置(teardown)步驟的機制。
它可以用來準備測試環境,例如建立測試資料庫、模擬 API 回應、設定測試變數等。
fixture
讓測試更加模組化、可重複使用,並提高測試的可讀性和可維護性。
fixture
的基本概念
在 pytest
中,fixture
是透過 @pytest.fixture
裝飾器來定義的,並且會被自動調用到測試函數中。
基本範例
import pytest
# 定義 fixture
@pytest.fixture
def sample_data():
return {"name": "Alice", "age": 25}
# 測試函數使用 fixture
def test_sample_data(sample_data):
assert sample_data["name"] == "Alice"
assert sample_data["age"] == 25
範例解析
@pytest.fixture
定義了一個sample_data
的fixture
,它回傳一個測試用的字典。- 測試函數
test_sample_data(sample_data)
直接使用這個fixture
,pytest
會自動將sample_data
的值傳入測試函數。 - 測試函數可以透過這個
fixture
取得測試資料,而不需要手動建立。
fixture
的作用
✅ 1. 減少重複程式碼
使用 fixture
可以避免在每個測試函數內部重複建立相同的測試資料。
✅ 2. 提供測試隔離
fixture
確保每次測試都從相同的初始狀態開始,避免測試之間相互影響,提高測試的可靠性。
✅ 3. 提供 setup
和 teardown
fixture
可以在測試開始前進行 初始化,在測試結束後 清理資源(如關閉資料庫連線、刪除測試檔案等)。
monkeypatch
的使用方式
替換函數的行為 (monkeypatch.setattr()
)
monkeypatch.setattr()
方法簡介
monkeypatch.setattr()
允許我們在測試期間 動態修改某個物件的屬性或方法,使其在測試執行時表現出不同的行為。
monkeypatch.setattr()
方法的參數
monkeypatch.setattr(target, name, value, raising=True)
參數 | 說明 |
---|---|
target | 需要修改的物件(模組、類別或實例) |
name | 物件的屬性或方法名稱(字串) |
value | 替換的新值(函數、類別或變數) |
raising | 預設為 True,若 name 不存在則拋出 AttributeError,設為 False 則忽略錯誤 |
示例 1:替換函數
假設我們有一個函數 get_current_time()
,它會回傳 time.time()
的結果,但我們希望測試時使用固定的值,以確保測試結果穩定。
原始程式
import time
def get_current_time():
return time.time()
測試時使用 monkeypatch.setattr()
替換 time.time()
def test_get_current_time(monkeypatch):
def fake_time():
return 1234567890 # 固定的時間戳
monkeypatch.setattr(time, "time", fake_time) # 替換 time.time 為 fake_time
assert get_current_time() == 1234567890 # 測試是否返回固定值
為什麼要這樣做?
time.time()
會隨著時間變動,但測試需要可重現的結果,因此我們用monkeypatch.setattr()
讓它回傳固定值。- 確保測試穩定性,避免測試因系統時間不同而產生不同結果。
- 不影響其他測試或系統函數,
monkeypatch
會在測試結束後自動還原time.time()
的原始行為。
示例 2:替換類別
有時候,我們想要模擬某個類別的行為,例如應用程式需要與 OpenAI API 互動,但我們不希望在測試時真正發送 API 請求。
原始程式
import openai
class OpenAIClient:
def __init__(self, api_key):
self.client = openai.Client(api_key=api_key)
def get_response(self, prompt):
response = self.client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
return response
測試時使用 monkeypatch.setattr()
替換 openai.OpenAI
def test_openai_client(monkeypatch):
class FakeChatCompletions:
def create(self, *args, **kwargs):
return {"choices": [{"message": {"content": "Mocked Response"}}]}
class FakeChat:
def __init__(self):
self.completions = FakeChatCompletions()
class FakeOpenAIClient:
def __init__(self, api_key):
self.api_key = api_key
self.chat = FakeChat() # ✅ 更新為新版 API 的結構
monkeypatch.setattr(openai, "Client", FakeOpenAIClient)
client = OpenAIClient(api_key="fake-key")
response = client.get_response("Hello!")
assert response["choices"][0]["message"]["content"] == "Mocked Response"
為什麼要這樣做?
- 避免真實 API 請求,防止影響 API 配額或測試速度。
- 模擬 API 回應,確保測試可以驗證 API 呼叫行為,而不依賴 OpenAI 伺服器的可用性。
- 提高測試的可控性,可以測試 API 回應的不同情境。
修改環境變數 (monkeypatch.setenv()
)
monkeypatch.setenv()
方法簡介
monkeypatch.setenv()
允許我們修改環境變數,以便在測試時模擬不同的設定。
monkeypatch.setenv()
方法的參數
monkeypatch.setenv(name, value, prepend=False)
參數 | 說明 |
---|---|
name | 環境變數名稱(字串) |
value | 環境變數的新值 |
prepend | 若為 True,則會將值附加到現有變數的前方 |
示例:修改 API 金鑰
假設我們的應用程式透過環境變數 API_KEY
來存取 API。
原始程式
import os
def get_api_key():
return os.getenv("API_KEY", "default-key")
測試時使用 monkeypatch.setenv()
設定環境變數
def test_get_api_key(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key") # 設定環境變數
assert get_api_key() == "test-key"
為什麼要這樣做?
- 模擬不同環境,確保程式可以根據不同的 API 金鑰運行。
- 避免修改系統環境變數,確保測試結束後環境變數不會受到影響。
移除屬性或方法 (monkeypatch.delattr()
)
monkeypatch.delattr()
方法簡介
monkeypatch.delattr()
允許我們在測試期間刪除某個屬性或方法,用來模擬該屬性不存在的情況。
monkeypatch.delattr()
方法的參數
monkeypatch.delattr(target, name, raising=True)
參數 | 說明 |
---|---|
target | 需要刪除屬性的物件 |
name | 屬性名稱(字串) |
raising | 預設為 True,若 name 不存在則拋出 AttributeError,設為 False 則忽略錯誤 |
示例:移除 os.remove()
方法
假設我們的程式需要刪除檔案,但我們希望測試當 os.remove()
不存在時的行為。
原始程式
import os
def delete_file(filename):
os.remove(filename)
測試時使用 monkeypatch.delattr()
def test_delete_file(monkeypatch):
monkeypatch.delattr(os, "remove") # 移除 os.remove 方法
try:
delete_file("test.txt")
except AttributeError:
assert True # 測試成功,因為 os.remove() 已被刪除
為什麼要這樣做?
- 測試系統函數缺失時的行為,確保程式能夠正確處理異常。
- 模擬不同環境,例如在受限系統中
os.remove()
可能不可用。
最佳實踐與注意事項
確保 monkeypatch
只在測試範圍內生效
monkeypatch
會在測試結束後自動還原變更,不過還是建議不要在測試範圍外使用它,以免影響其他測試案例。
使用 monkeypatch
時應確保變更合理
過度使用 monkeypatch
可能會讓測試變得過於依賴 mock,而無法驗證程式的真實行為。
因此,應該只在必要時使用 monkeypatch
,並搭配其他測試方法(如整合測試)。
搭配 pytest
使用 monkeypatch
monkeypatch
是 pytest
提供的 fixture,因此應搭配 pytest
來運行測試,以確保 monkeypatch
能夠正確運作。
結論
monkeypatch
是 Python 測試中非常有用的工具,它允許我們輕鬆地修改函數、類別、環境變數等,使測試更可控、更穩定。
在測試過程中,透過適當地運用 monkeypatch
,我們可以有效避免不必要的 API 請求、模擬不同的測試場景,並確保程式能夠在各種情境下正常運作。
希望這篇文章能幫助你更深入了解 monkeypatch
,並在實際測試中靈活運用它! 🚀