pytest 匯入機制和 sys.path
/PYTHONPATH
¶
匯入模式¶
作為測試架構的 pytest 需要匯入測試模組和 conftest.py
檔案才能執行。
在 Python 中匯入檔案並非微不足道的程序,因此匯入程序的各個方面都可以透過 --import-mode
命令列旗標來控制,它可以假設這些值
prepend
(預設):包含每個模組的目錄路徑會插入sys.path
的開頭(如果尚未存在),然後使用importlib.import_module
函數匯入。強烈建議將測試模組安排為套件,方法是在包含測試的目錄中新增
__init__.py
檔案。這會讓測試成為適當 Python 套件的一部分,讓 pytest 能解析它們的全名(例如tests.core.test_core
,表示tests.core
套件中的test_core.py
)。如果測試目錄樹未安排為套件,則每個測試檔案與其他測試檔案相比都需要有獨一無二的名稱,否則 pytest 會在找到兩個名稱相同的測試時產生錯誤。
這是經典的機制,可以追溯到 Python 2 仍受支援的時期。
append
:包含每個模組的目錄會附加到sys.path
的結尾(如果尚未存在),然後使用importlib.import_module
匯入。這能更好地針對已安裝的套件版本執行測試模組,即使受測套件具有相同的匯入根目錄。例如
testing/__init__.py testing/test_pkg_under_test.py pkg_under_test/
當使用
--import-mode=append
時,測試會針對pkg_under_test
的已安裝版本執行,而使用prepend
時,則會選取本機版本。這種混淆正是我們提倡使用 src-layouts 的原因。與
prepend
相同,當測試目錄樹未以套件方式排列時,需要測試模組名稱保持唯一,因為模組會在匯入後放入sys.modules
中。
importlib
:此模式使用importlib
提供的更精細控制機制來匯入測試模組,而不會變更sys.path
。此模式的優點
pytest 根本不會變更
sys.path
。測試模組名稱不需要保持唯一性,pytest 會根據
rootdir
自動產生唯一名稱。
缺點
測試模組無法互相匯入。
無法匯入測試目錄中的測試工具模組(例如包含與測試相關函式/類別的
tests.helpers
模組)。建議將測試工具模組與應用程式/函式庫程式碼放在一起,例如app.testing.helpers
。重要事項:「測試工具模組」是指由其他測試直接匯入的函式/類別;這不包括應放置在
conftest.py
檔案中的固定裝置,這些固定裝置應與測試模組放在一起,並由 pytest 自動偵測。
其運作方式如下
給定特定模組路徑,例如
tests/core/test_models.py
,會衍生出正規名稱,例如tests.core.test_models
,並嘗試匯入。對於非測試模組,如果它們可透過
sys.path
存取,此方法便會奏效,因此例如.env/lib/site-packages/app/core.py
可以作為app.core
匯入。這會在外掛程式匯入非測試模組(例如 doctesting)時發生。如果此步驟成功,便會傳回模組。
對於測試模組,除非它們可以從
sys.path
存取,否則此步驟將失敗。如果前一個步驟失敗,我們會使用
importlib
設施直接匯入模組,這讓我們可以在不變更sys.path
的情況下匯入模組。由於 Python 要求模組也必須在
sys.modules
中可用,因此 pytest 會根據模組在rootdir
中的相對位置為模組衍生一個唯一名稱,並將模組新增至sys.modules
。例如,
tests/core/test_models.py
最終會作為模組tests.core.test_models
匯入。
新增於版本 6.0。
注意
最初我們打算在未來版本中將 importlib
設定為預設值,但現在很明顯它有自己的一組缺點,因此在可預見的未來,預設值仍將維持 prepend
。
注意
預設情況下,pytest 不會嘗試自動解析命名空間套件,但這可以使用 consider_namespace_packages
組態變數來變更。
prepend
和 append
匯入模式場景¶
以下是使用 prepend
或 append
匯入模式時,pytest 需要變更 sys.path
以匯入測試模組或 conftest.py
檔案的場景清單,以及使用者可能因此遇到的問題。
套件內部的測試模組 / conftest.py
檔案¶
考慮這個檔案和目錄配置
root/
|- foo/
|- __init__.py
|- conftest.py
|- bar/
|- __init__.py
|- tests/
|- __init__.py
|- test_foo.py
執行時
pytest root/
pytest 會找到 foo/bar/tests/test_foo.py
並意識到它是一個套件的一部分,因為同一個資料夾中有一個 __init__.py
檔案。然後它會向上搜尋,直到找到最後一個仍包含 __init__.py
檔案的資料夾,以找到套件根目錄(在本例中為 foo/
)。為了載入模組,它會將 root/
插入 sys.path
的前面(如果尚未存在),以將 test_foo.py
作為模組 foo.bar.tests.test_foo
載入。
相同的邏輯套用於 conftest.py
檔案:它會作為 foo.conftest
模組匯入。
當測試存在於套件中時,保留完整的套件名稱非常重要,以避免問題並允許測試模組具有重複的名稱。這也在 Python 測試偵測慣例 中詳細討論。
獨立測試模組 / conftest.py
檔案¶
考慮這個檔案和目錄配置
root/
|- foo/
|- conftest.py
|- bar/
|- tests/
|- test_foo.py
執行時
pytest root/
pytest 會找到 foo/bar/tests/test_foo.py
並意識到它不是套件的一部分,因為同一個資料夾中沒有 __init__.py
檔案。然後它會將 root/foo/bar/tests
加入 sys.path
,以將 test_foo.py
作為模組 test_foo
匯入。對 conftest.py
檔案也是如此,方法是將 root/foo
加入 sys.path
,以將其作為 conftest
匯入。
因此,此佈局不能有同名的測試模組,因為它們都會匯入至全域匯入命名空間。
這也會在 Python 測試偵測慣例 中詳細討論。
呼叫 pytest
與 python -m pytest
¶
使用 pytest [...]
而不是 python -m pytest [...]
執行 pytest 會產生近乎等效的行為,但後者會將目前目錄新增至 sys.path
,這是標準 python
行為。