pytest のインポートメカニズムと sys.path/PYTHONPATH¶
インポートモード¶
pytest はテストフレームワークとして、実行時にテストモジュールと conftest.py ファイルをインポートする必要があります。
Python でファイルをインポートすることは些細なプロセスではないため、インポートプロセスの様々な側面は --import-mode コマンドラインフラグで制御でき、以下の値を指定できます:
prepend(デフォルト): 各モジュールを含むディレクトリパスがsys.pathの 先頭 に挿入され (まだ存在しない場合)、その後importlib.import_module関数でインポートされます。テストを含むディレクトリに
__init__.pyファイルを追加して、テストモジュールをパッケージとして構成することを強く推奨します。 これによりテストが適切な Python パッケージの一部となり、pytest が完全な名前を解決できるようになります (例えばtests.coreパッケージ内のtest_core.pyはtests.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ファイルに配置され、テストモジュールと一緒に自動的に検出されるフィクスチャは含まれません。
動作の仕組み:
特定のモジュールパス (例:
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としてインポートされます。
Added in version 6.0.
注釈
当初は将来のリリースで importlib をデフォルトにする予定でしたが、現在では独自の欠点があることが明らかになったため、予見可能な将来にわたってデフォルトは prepend のままとなります。
注釈
デフォルトでは、pytest は名前空間パッケージを自動的に解決しようとしませんが、これは consider_namespace_packages 設定変数で変更できます。
prepend と append インポートモードのシナリオ¶
以下は、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/tests を sys.path に追加します。 同じことが conftest.py ファイルにも行われ、root/foo を sys.path に追加して conftest としてインポートします。
このため、このレイアウトでは同じ名前のテストモジュールを持つことはできません。 すべてがグローバルインポートネームスペースにインポートされるためです。
これについては Python テストの発見に関する規則 で詳しく説明しています。
pytest と python -m pytest の呼び出し¶
python -m pytest [...] の代わりに pytest [...] で pytest を実行すると、ほぼ同等の動作が得られますが、後者は現在のディレクトリを sys.path に追加します。 これは標準の python の動作です。