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 自動偵測。

    其運作方式如下

    1. 給定特定模組路徑,例如 tests/core/test_models.py,會衍生出正規名稱,例如 tests.core.test_models,並嘗試匯入。

      對於非測試模組,如果它們可透過 sys.path 存取,此方法便會奏效,因此例如 .env/lib/site-packages/app/core.py 可以作為 app.core 匯入。這會在外掛程式匯入非測試模組(例如 doctesting)時發生。

      如果此步驟成功,便會傳回模組。

      對於測試模組,除非它們可以從 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