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 自動發現。

    它的工作方式如下

    1. 給定一個特定的模組路徑,例如 tests/core/test_models.py,導出一個規範名稱,如 tests.core.test_models,並嘗試導入它。

      對於非測試模組,如果它們可以通過 sys.path 訪問,這將起作用。因此,例如,.env/lib/site-packages/app/core.py 將可作為 app.core 導入。當插件導入非測試模組時(例如 doctest)會發生這種情況。

      如果此步驟成功,則返回模組。

      對於測試模組,除非它們可以從 sys.path 訪問,否則此步驟將失敗。

    2. 如果上一步失敗,我們將使用 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 配置變數來更改。

另請參閱

pythonpath 配置變數。

consider_namespace_packages 配置變數。

選擇測試佈局.

prependappend 導入模式情境

以下列出了在使用 prependappend 導入模式時,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 測試發現慣例 中詳細討論。

調用 pytestpython -m pytest

使用 pytest [...] 而不是 python -m pytest [...] 運行 pytest 會產生幾乎相同的行為,除了後者會將目前目錄添加到 sys.path,這是標準的 python 行為。

另請參閱 通過 python -m pytest 調用 pytest