pytest 導入機制與 sys.path
/PYTHONPATH
¶
匯入模式¶
pytest 作為一個測試框架,需要導入測試模組和 conftest.py
檔案才能執行。
在 Python 中導入檔案是一個非簡單的過程,因此導入過程的各個方面可以通過 --import-mode
命令列標誌來控制,它可以採用以下值
prepend
(預設):如果目錄路徑尚不存在,則包含每個模組的目錄路徑將插入到sys.path
的開頭,然後使用importlib.import_module
函數導入。強烈建議通過將
__init__.py
檔案添加到包含測試的目錄中,將測試模組安排為套件。這將使測試成為適當 Python 套件的一部分,允許 pytest 解析它們的完整名稱(例如,對於tests.core.test_core
套件內的test_core.py
,名稱為tests.core.test_core
)。如果測試目錄樹未安排為套件,則每個測試檔案都需要具有與其他測試檔案相比唯一的名稱,否則 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 佈局 的原因。與
prepend
相同,當測試目錄樹未安排在套件中時,需要測試模組名稱是唯一的,因為模組在導入後會放入sys.modules
中。
importlib
:此模式使用importlib
提供的更精細的控制機制來導入測試模組,而無需更改sys.path
。此模式的優點
pytest 將完全不會更改
sys.path
。測試模組名稱不需要是唯一的 – pytest 將根據
rootdir
自動產生唯一的名稱。
缺點
測試模組無法互相導入。
測試目錄中的測試實用模組(例如包含與測試相關的函數/類別的
tests.helpers
模組)是不可導入的。在這種情況下,建議將測試實用模組與應用程式/庫代碼放在一起,例如app.testing.helpers
。重要提示:通過「測試實用模組」,我們指的是其他測試直接導入的函數/類別;這不包括 Fixture,Fixture 應放置在
conftest.py
檔案中,與測試模組一起,並由 pytest 自動發現。
它的工作方式如下
給定一個特定的模組路徑,例如
tests/core/test_models.py
,導出一個規範名稱,如tests.core.test_models
,並嘗試導入它。對於非測試模組,如果它們可以通過
sys.path
訪問,這將起作用。因此,例如,.env/lib/site-packages/app/core.py
將可作為app.core
導入。當插件導入非測試模組時(例如 doctest)會發生這種情況。如果此步驟成功,則返回模組。
對於測試模組,除非它們可以從
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
行為。