できない.dev

pytest でテストから自作モジュールを import できない(ModuleNotFoundError)

pytest はプロジェクトのルートではなく、テストファイル側のディレクトリを sys.path に足す。
src レイアウトなら設定の pythonpath か編集可能インストール、フラットな構成なら python -m pytest か tests/__init__.py で解決する。

公開:

要約

アプリのコードは python から普通に動くのに、pytest でテストを実行すると収集の段階で止まる。

ImportError while importing test module '/app/tests/test_calc.py'.
Hint: make sure your test modules/packages have valid Python names.
...
E   ModuleNotFoundError: No module named 'myapp'

pytest は既定の import モード(prepend)で、テストファイルから上へたどって最初に __init__.py が無いディレクトリを sys.path の先頭に足す。tests/ に __init__.py が無ければ、足されるのは tests/ だけで、プロジェクトのルートや src/ は入らない。
これがエラーの正体だ。

よくある原因

  1. src レイアウトで src/ が検索パスに無い: src/myapp/ に置いたパッケージは、インストールするか検索パスに足さない限り import できない。
  2. フラットな構成で pytest を実行している: ルートに myapp/ がある構成でも、pytest コマンドはカレントディレクトリを sys.path に足さない。
  3. --import-mode=importlib にしている: 公式ドキュメントは新規プロジェクトに importlib モードを勧めているが、このモードでは pytest が sys.path を変更しない。conftest.py や __init__.py の位置で検索パスを足してもらう方法が効かなくなる。
  4. 別の Python の pytest を起動している: グローバルに入った pytest が先に見つかると、仮想環境に入れたパッケージは見えない。

解決策

1. src レイアウトは pythonpath か編集可能インストール

pytest 7.0 以降は、設定ファイルの pythonpath で検索パスを足せる。
パスは rootdir からの相対で、テストセッションの間だけ有効になる。

# pyproject.toml
[tool.pytest.ini_options]
pythonpath = ["src"]

一時的に試すなら PYTHONPATH=src pytest でもよい。
パッケージとして配布する構成なら、pyproject.toml にビルド設定を書いたうえで編集可能インストールにすると、ソースを直してもそのままテストに反映される。

pip install -e .

2. フラットな構成は python -m pytest

python -m pytest は Python の標準の動きとしてカレントディレクトリを sys.path に足すので、ルートの myapp/ が見える。

python -m pytest

毎回 pytest だけで動かしたい場合は、ルートに空の conftest.py を置くか、tests/__init__.py を置く。
前者はルートが、後者は __init__.py が無い最初の上位ディレクトリ(ここではルート)が sys.path に入る。

3. importlib モードではインストールを前提にする

importlib モードでは、手順 2 のうちルートの conftest.py と tests/__init__.py は効かない。
手順 1 の pip install -e . か pythonpath の設定を使う。python -m pytest は Python 自身がカレントディレクトリを足すので、このモードでも効く。

4. 実行している Python を揃える

python -c "import sys; print(sys.executable)"
python -m pytest

python -m pytest は、いま使っている Python に入った pytest を起動するので、パッケージと pytest の環境が食い違わない。

この記事は役立ちましたか?