Pytest 固件
固定器提供明確且可靠的測試背景。 自訂 fixture 是以 @pytest.fixture 裝飾的 Python 函式。 你可以用它們來安排前置條件,例如測試資料、環境變數、資料庫連線,或測試執行前所需的檔案。 Fixture 也可以定義清理步驟,確保在測試或 fixture 範圍完成後清除資源。
測試透過宣告與該 fixture 名稱相同的引數來要求 fixture。 Pytest 會找到 fixture、執行 fixture,並將回傳或產生的值傳入測試。 欲了解更多關於 fixture 生命週期的資訊,請參閱 pytest 文件中的 fixtures。
在設定或清除重複使用、變得複雜或在命名時變才得更清楚時,fixture 最為實用。 對於簡單的一次性值,直接將設定程式碼保留在測試中會比較容易閱讀。
pytest 裝置的主要特性包括:
-
範圍控制:夾具可利用參數
scope控制 pytest 建立與銷毀夾具值的頻率。 更廣泛的範圍可以提升效能,但它們共享狀態的時間也更長。 -
設定與清理:Fixture 可使用
yield清除。 進階 fixture 也能接收內建的requestfixture (在本單元稍後會介紹),用來檢查發出要求的測試,並呼叫request.addfinalizer()來註冊清除回呼。 - 依賴注入:測試與夾具以參數形式請求夾具,使相依關係明確化。
- 可重複使用與模組化:夾具可定義一次,並在測試功能、模組、目錄或專案間重複使用。
-
自動啟用:透過
@pytest.fixture(autouse=True)定義的 fixture 會在其可用性範圍內對每項測試執行,無需作為引數列出。 這個選項要謹慎使用,因為它能隱藏測試相依關係。
建立暫存盤裝置
撰寫與檔案互動的測試時,通常需要獨立路徑,且不會與其他測試衝突,也不會依賴硬編碼的位置。 Pytest 提供內建的臨時目錄夾具,這些通常是新測試的最佳選擇,因為 pytest 會建立獨特的目錄並管理其保留。 然而,建立一個小型自訂 fixture 是了解 fixture 要求與回傳值的實用方法。
以下fixture 會回傳工廠函式。 燈具本身在設定時不會建立檔案。 相反地,當測試呼叫create()時,巢狀tmp_file()函數會建立一個檔案:
from pathlib import Path
import tempfile
import pytest
@pytest.fixture
def tmp_file():
def create(contents=""):
temp = tempfile.NamedTemporaryFile(delete=False)
try:
path = Path(temp.name)
finally:
temp.close()
path.write_text(contents, encoding="utf-8")
return path
return create
燈具使用 Python 的 tempfile.NamedTemporaryFile() 函式,搭配 delete=False,因此關閉後檔案仍然存在。 檔案代柄在測試收到路徑前會關閉,這在測試需要重新開啟或刪除檔案時非常重要,尤其是在 Windows 上。
以下範例顯示測試可能使用 fixture 的方式:
def test_file(tmp_file):
path = tmp_file("ready")
assert path.read_text(encoding="utf-8") == "ready"
在此設置中,tmp_file 是設備的名稱,測試接收設備返回的 create() 函數。 在測試中呼叫 tmp_file() 會建立檔案,寫入請求的內容,並回傳 pathlib.Path 物件。
Note
結合固定裝置與上下文管理器時要小心。 如果 fixture 從 with 區塊內部傳回路徑或控制代碼,內容管理員可能會在測試使用前關閉或刪除該資源。 當測試需要受控資源,請將內容管理員維持在 yield 之間開啟,或在傳回前刻意關閉資源,並在稍後進行清理。
範圍管理
預設情況下,pytest 燈具會使用 function 作用範圍。 函式範圍的 fixture 會在測試要求時建立,並在測試結束時終結。 如果 fixture 使用清理程式碼,pytest 會在 fixture 範圍結尾清理程式碼。
Pytest 也允許更廣泛的 fixture 範圍,以最佳化昂貴的設定工作。 內建的裝置範疇名稱如下:
-
function:預設的瞄準鏡。 每個測試函式會建立一個 fixture。 -
class:每個測試類別會建立一個 fixture。 -
module:此夾具僅為該模組創建一次。 -
package:Fixture 會在套件最後一次測試後定義和清理的位置為套件建立一次,包括子套件和子目錄中的測試。 -
session:測試階段中會建立一次 fixture。
對於進階情況,scope 也可以為一個可調用物件,回傳這些範圍名稱之一。 當 Pytest 定義夾具並傳遞關鍵字參數 fixture_name 和 config時,會呼叫該函式一次。 詳情請參考動態 範圍 上的 pytest 文件。
在這個內容中,建立一次指的是 pytest 快取該範圍的 fixture 值。 例如,模組範圍的測試 fixture 可以在一個模組中被多個測試要求,但這些測試會收到相同的快取 fixture 值。 參數化 fixture 是例外狀況:pytest 可為範圍內設定的每個參數設定建立新值。
除非共用設定安全且實用,否則請將 fixtures 保持在函式範圍。 可變物件、開啟的連線、環境變更和檔案在被更廣泛的範圍快取時,可能會在測試間洩漏狀態。
已下是含模組範圍的 tmp_file fixture:
from pathlib import Path
import tempfile
import pytest
@pytest.fixture(scope="module")
def tmp_file():
def create(contents=""):
temp = tempfile.NamedTemporaryFile(delete=False)
try:
path = Path(temp.name)
finally:
temp.close()
path.write_text(contents, encoding="utf-8")
return path
return create
因為這個夾具會回傳出廠函數,並且 scope="module" 快取模組的出廠函式。 工廠每次測試呼叫 tmp_file()時仍會建立一個新檔案。 如果夾具直接建立並回傳檔案路徑,則模組內的每個測試都會收到相同的路徑。
清理管理
舊版本 tmp_file 的燈具會建立暫存檔案,但不會移除這些檔案。 Pytest 推薦使用 yield 夾具進行簡單的清理。 在 yield 之前的程式碼執行設定並提供給測試一個值。 即使設定完成後測試失敗,yield 之後的程式碼也會在清理時執行。 如果 yield fixture 在達到 yield 之前引發錯誤,pytest 不會在 yield 之後執行清理程式碼,但仍會清理相同測試中已成功完成的 fixture。
以下版本會追蹤工廠建立的每個檔案,並在測試結束後嘗試刪除這些檔案:
from pathlib import Path
import tempfile
import pytest
@pytest.fixture
def tmp_file():
paths = []
def create(contents=""):
temp = tempfile.NamedTemporaryFile(delete=False)
try:
path = Path(temp.name)
finally:
temp.close()
paths.append(path)
path.write_text(contents, encoding="utf-8")
return path
yield create
for path in paths:
try:
path.unlink()
except FileNotFoundError:
pass
在預設 function 範圍內,pytest 會在每次請求 tmp_file的測試後執行清理程式碼。 如果你把 fixture 改成 scope="module",pytest 會在完成該模組的測試後,在模組清理期間執行清除。 當多個 yield 燈具同時啟用時,pytest 會以設定順序反向執行拆除程式碼。
清理之所以用 try/except FileNotFoundError ,是因為測試可能會刪除檔案本身。 在 Windows 上,如果測試使檔案保持開啟,刪除也可能因 PermissionError 失敗,因此請務必關閉測試所建立的控制代碼。 當你需要更多控制時,也可以用 request.addfinalizer() 來清理。 只有在資源建立後才註冊完成項,因為即使 fixture 之後引發問題,pytest 在清理時仍會執行新增的完成項。 對於簡單的清除工作,yield fixtures 通常較易於讀取。
使用 conftest.py
你可以將共用的夾具儲存在一個名為 conftest.py的檔案中,而不是在每個測試檔案中定義。 在 conftest.py 中的 fixture 會自動可供相同目錄及其子目錄中的測試使用,無需明確匯入。
你可以在測試套件中放置多個 conftest.py 檔案。 Pytest 從要求測試的角度解決 fixture 可用性:測試類別或模組中的 fixture 會加入區域範圍,接著是相同的目錄及父目錄中檔案的 conftest.py fixture。 測試可以向上搜尋 fixture,但不會向下搜尋同層級目錄或子目錄。 外掛程式 fixture 會在區域 fixture 後搜尋。 不要從 conftest.py 測試檔案匯入;讓 pytest 來發現。
探索內建裝置
Pytest 有許多專為簡化測試而設計的內建裝置。 這些 fixture 負責常見的設定與清除工作,讓您能專注於撰寫測試判斷提示。 欲查看完整清單,請參閱 pytest 內建功能參考資料。
主要內建裝置包括:
-
cache: 在 pytest 執行中儲存並檢索值。 -
capsys:擷取寫入至stdout和stderr的文字。 -
tmp_path: 提供pathlib.Path一個臨時目錄物件,該目錄對每個測試函式調用(包括每個參數化案例)獨一無二。 -
tmpdir提供一個暫存目錄作為遺留py.path.local物件。 對於新的測試,pytest 推薦tmp_path。 -
monkeypatch:暫時修改類別、函式、字典、環境變數及其他物件。 -
request:提供目前執行中的測試或 fixture 資訊,並支援完成項。
其他常用的內建裝置包括 caplog (擷取日誌記錄)、 capfd (擷取檔案描述符層級輸出)、 tmp_path_factory (建立具有會話範圍的暫存目錄)以及 pytestconfig (暴露活躍的 pytest 配置)。 完整清單請參閱 pytest 內建裝置參考資料。
下一個練習是使用 tmp_path 和 pathlib.Path 在每個測試目錄中建立暫存檔案。 你可能會在現有的 pytest 專案中遇到 tmpdir ,但對於新程式碼,建議 tmp_path使用 。 預設情況下,pytest 會保留最近幾次測試執行的暫存目錄,因此執行後會看到最近由 pytest 管理的臨時資料夾。 欲了解更多資訊,請參閱 pytest 關於 暫存目錄與檔案的文件。
Monkeypatching 在測試中的角色
先前的 fixture 負責建立及清理測試資源。 夾具的另一個常見用途是在需要將程式碼與外部狀態隔離的測試中,進行 臨時狀態修改 。 在這種情況下,pytest 提供內建 monkeypatch fixture,而這個設置本身就是 pytest fixture,也是自動清理的範例。
測試依賴外部資源、全域配置或作業系統狀態的程式碼可能具有挑戰性。
monkeypatch fixture 可讓您暫時變更屬性、字典值、環境變數、目前的工作目錄或 sys.path 以進行測試。 Pytest 會在請求測試函式或 fixture 完成後自動還原這些變更。
這裡有一個例子,是在一個小型專案模組中替換了一個函數屬性,用於一個測試。 修補範圍狹窄的應用程式程式碼(而非 pytest 本身使用的標準函式庫函式)能保持測試焦點,避免意外破壞 pytest 自身的行為:
# project_module.py
def get_user_id():
# Imagine this calls a remote service.
return "real-user"
# test_project.py
import project_module
def test_user_id(monkeypatch):
monkeypatch.setattr(project_module, "get_user_id", lambda: "test-user")
assert project_module.get_user_id() == "test-user"
你也可以指定目標為點線匯入路徑字串。 Pytest 在套用修補程式時會解析並匯入命名模組,這在測試檔還不需要模組時很方便。 本範例中,後面出現的 import project_module 僅用於讓測試中可以參考補丁函數,用於斷言。
# test_project.py
def test_user_id_by_path(monkeypatch):
monkeypatch.setattr("project_module.get_user_id", lambda: "test-user")
import project_module
assert project_module.get_user_id() == "test-user"
除了使用 setattr() 設定屬性和覆寫方法之外,monkeypatch fixture 還能刪除使用 delattr() 的屬性,設定和刪除環境變數 (setenv,delenv),設定及刪除字典項目 (setitem,delitem),新增 sys.path 前的值 (syspath_prepend),變更目前工作目錄 (chdir),並使用 context() 建立巢狀的修補範圍。 這些 setattr() 和 delattr() 的預設 raising=True 行為有助於攔截拼字錯誤的目標。 在測試或治具拆除繼續進行之前,若補丁需要先被移除,請使用 monkeypatch.context() 。 欲了解更多資訊,請參閱 pytest 關於 monkeypatching 的文件。
雖然 monkeypatch 很強大,但請謹慎使用:
-
程式碼清晰度與維護:過度使用
monkeypatch或以複雜方式使用,會使測試更難理解與維護。 - 測試有效性:Monkeypatching 能創造出在與生產行為截然不同的人工條件下通過的測試。
- 過度依賴實作細節:修補內部實作細節的測試,當底層程式碼變更時,可能會變得脆弱。
-
除錯複雜度:當補丁改變基本應用程式行為時,除錯
monkeypatch測試會變得更困難。
當它能幫助分離出一個專注的行為時,請使用 monkeypatch ,並盡量保持每個補丁都小且明確。