pytest で同名テストファイルが ImportPathMismatchError になる原因と解決策
複数のディレクトリにあるテストをまとめて流したら、収集の段階でこんなエラーが出て止まりました。
==================================== ERRORS ====================================
___________ ERROR collecting component-b/tests/test_completeness.py ____________
import file mismatch:
imported module 'test_completeness' has this __file__ attribute:
<path>/component-a/tests/test_completeness.py
which is not the same as the test file we want to collect:
<path>/component-b/tests/test_completeness.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
import file mismatch: から始まるこの出力は、pytest 内部では ImportPathMismatchError という例外として投げられます(ソースは _pytest/pathlib.py)。
これを見て、どう直せばいいか調べた記録です。同じ画面で手が止まった人向けに書いています。
この記事で分かることは次の3点です。
- なぜ別ディレクトリの同名テストファイルが衝突するのか
- いちばん手早い直し方(実機で確認済み)
- ほかの直し方との使い分け(リネーム不要の手も含む)
検証は pytest 9.1.1 / Python 3.13 で、実際に再現と解消を確認しました。
何が起きたか
複数のコンポーネントにそれぞれテストを置いた、こんな構成でした。
project/
component-a/tests/test_completeness.py
component-b/tests/test_completeness.py
中身は別物ですが、ファイル名はどちらも test_completeness.py です。
プロジェクトのルートから、両方のディレクトリをまとめて指定して実行します。
pytest component-a/tests/ component-b/tests/
すると、片方は収集できるのにもう片方の収集で冒頭の import file mismatch: が出て、テストが1件も走らないまま止まりました。
テストコード自体には文法エラーも import エラーもありません。
pytest component-a/tests/ だけ、あるいは pytest component-b/tests/ だけなら、それぞれ問題なく通ります。両方を同時に集めようとしたときだけ落ちる、という症状でした。
なぜ起きるのか
原因は、pytest がデフォルトの import モード(prepend)で「テストファイルのあるディレクトリを sys.path の先頭に挿し込み、ファイル名(basename)をそのままトップレベルのモジュール名として import する」ためです1。
__init__.py を置いていないテストファイルは、basename がモジュール名になります。
component-a/tests/test_completeness.py も component-b/tests/test_completeness.py も、どちらも test_completeness という同じモジュール名で取り込まれます。
先に片方が test_completeness として import 済みになっているところへ、別ファイルを同じ名前で取り込もうとして「同じモジュール名なのに __file__ が違う」と弾かれる、というわけです。
どう直したか
ファイル名にコンポーネント名のプレフィックスを付けてリネームしたら、それだけで通りました。
project/
component-a/tests/test_a_completeness.py
component-b/tests/test_b_completeness.py
basename が test_a_completeness と test_b_completeness で別物になるので、モジュール名がぶつかりません。
============================== 2 passed ===============================
ルートに置いた conftest.py の fixture も、リネーム後そのまま解決できました(実機で確認)。
副次的に、テスト名を見ればどのコンポーネントのものか分かるようになります。
tests/ 配下にずらりと並んだときの見通しも良くなりました。
構成を変えずファイル名だけ変えればいいので、まず試す手としてはこれが手軽でした。
リネームせずに直すなら
ファイル名を変えたくないときは、テストのディレクトリを Python パッケージにする手もあります。
各ディレクトリに空の __init__.py を置くと、そのディレクトリはパッケージ扱いになり、モジュール名が basename 単体ではなく階層込みの完全修飾名(例: component_a.tests.test_completeness)になります。
名前にディレクトリ構造が入るため、別ディレクトリの同名ファイルとぶつかりません。
※完全修飾名がコンポーネント間で別物になるところまで、親ディレクトリにも __init__.py を置きます。
pytest 公式もテストをパッケージとして並べることを推奨しています。
設定を変えてよければ、--import-mode=importlib にすると sys.path を触らない取り込み方になり、basename の重複自体が問題になりません。
(コメントいただきありがとうございました)
次に同じ画面を見たら
import file mismatch: を見たら、まず「複数のディレクトリに同名のテストファイルがないか」を考える。
横展開でテストをコピーしていくとき(コンポーネントごとに同じ test_xxx.py を増やすときなど)は、最初からファイル名にコンポーネント名を入れておくと、この衝突を踏まずに済みます。
参考リンク
-
pytest documentation — Import modes / pythonpath(prepend・importlib の機序と制約、既定の import モード) ↩