pytest のインポートメカニズムと sys.path/PYTHONPATH

インポートモード

pytest はテストフレームワークとして、実行時にテストモジュールと conftest.py ファイルをインポートする必要があります。

Python でファイルをインポートすることは些細なプロセスではないため、インポートプロセスの様々な側面は --import-mode コマンドラインフラグで制御でき、以下の値を指定できます:

  • prepend (デフォルト): 各モジュールを含むディレクトリパスが sys.path先頭 に挿入され (まだ存在しない場合)、その後 importlib.import_module 関数でインポートされます。

    テストを含むディレクトリに __init__.py ファイルを追加して、テストモジュールをパッケージとして構成することを強く推奨します。 これによりテストが適切な Python パッケージの一部となり、pytest が完全な名前を解決できるようになります (例えば tests.core パッケージ内の test_core.pytests.core.test_core となります)。

    テストディレクトリツリーがパッケージとして構成されていない場合、各テストファイルは他のテストファイルと比較して一意の名前を持つ必要があります。 そうでない場合、pytest は同じ名前のテストが 2 つ見つかるとエラーを発生させます。

    これは 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: このモードは sys.path を変更せずにテストモジュールをインポートするために、importlib が提供するより細かい制御メカニズムを使用します。

    このモードの利点:

    • pytest は sys.path を一切変更しません。

    • テストモジュール名は一意である必要がありません -- pytest は rootdir に基づいて自動的に一意の名前を生成します。

    欠点:

    • テストモジュール同士でインポートできません。

    • テストディレクトリ内のテストユーティリティモジュール (例: テスト関連の関数/クラスを含む tests.helpers モジュール) はインポートできません。 この場合は、テストユーティリティモジュールをアプリケーション/ライブラリコードと一緒に配置することを推奨します (例: app.testing.helpers)。

      重要: 「テストユーティリティモジュール」とは、他のテストから直接インポートされる関数/クラスを指します。 これには conftest.py ファイルに配置され、テストモジュールと一緒に自動的に検出されるフィクスチャは含まれません。

    動作の仕組み:

    1. 特定のモジュールパス (例: tests/core/test_models.py) が与えられると、tests.core.test_models のような正規の名前を導出し、インポートを試みます。

      非テストモジュールの場合、sys.path 経由でアクセス可能であればこれが機能します。 例えば、.env/lib/site-packages/app/core.pyapp.core としてインポート可能です。 これはプラグインが非テストモジュールをインポートする場合 (例: doctest) に発生します。

      このステップが成功すると、モジュールが返されます。

      テストモジュールの場合、sys.path から到達可能でない限り、このステップは失敗します。

    2. 前のステップが失敗した場合、importlib の機能を使用してモジュールを直接インポートします。 これにより、sys.path を変更せずにインポートできます。

      Python ではモジュールを sys.modules でも利用可能にする必要があるため、pytest は rootdir からの相対的な位置に基づいて一意の名前を導出し、そのモジュールを sys.modules に追加します。

      例えば、tests/core/test_models.py は最終的にモジュール tests.core.test_models としてインポートされます。

    Added in version 6.0.

注釈

当初は将来のリリースで importlib をデフォルトにする予定でしたが、現在では独自の欠点があることが明らかになったため、予見可能な将来にわたってデフォルトは prepend のままとなります。

注釈

デフォルトでは、pytest は名前空間パッケージを自動的に解決しようとしませんが、これは consider_namespace_packages 設定変数で変更できます。

prependappend インポートモードのシナリオ

以下は、pytest がテストモジュールや conftest.py ファイルをインポートするために sys.path を変更する必要がある prepend または append インポートモードを使用する際のシナリオと、ユーザーが遭遇する可能性がある問題のリストです。

パッケージ内のテストモジュール / 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 ファイルがあることからパッケージの一部であることを認識します。 次に、パッケージの ルート (この場合は foo/) を見つけるために、まだ __init__.py ファイルを含む最後のフォルダまで上向きに検索します。 モジュールをロードするために、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 ファイルがないことからパッケージの一部ではないことを認識します。 次に、test_foo.pyモジュール test_foo としてインポートするために root/foo/bar/testssys.path に追加します。 同じことが conftest.py ファイルにも行われ、root/foosys.path に追加して conftest としてインポートします。

このため、このレイアウトでは同じ名前のテストモジュールを持つことはできません。 すべてがグローバルインポートネームスペースにインポートされるためです。

これについては Python テストの発見に関する規則 で詳しく説明しています。

pytestpython -m pytest の呼び出し

python -m pytest [...] の代わりに pytest [...] で pytest を実行すると、ほぼ同等の動作が得られますが、後者は現在のディレクトリを sys.path に追加します。 これは標準の python の動作です。

関連項目 python -m pytest を通じて pytest を呼び出す