45395 - シコウサクゴ -

conftest.py の sys.path.insert に依存したテストを CI で透視する:絶対 import 統一と grep 禁則

2026-05-05
AI駆動開発
AI駆動開発
CICD
コード品質
Python
pytest
リファクタリング
Last updated:2026-05-07
9 Minutes
1627 Words

ローカルでは pytest が全部通るのに、CI で ModuleNotFoundError が出る。よく見るとテストの import 文と conftest.py の sys.path 操作 がマッチしていなくて、ローカルだけたまたま通っていた、というケースを何度か踏みました。

最終的に「相対 import 禁止 + sys.path.insert 禁止」を CI で強制する方針に切り替えた記録です。

何が起きるのか

症状

1
$ pytest tests/
2
============ 全 124 テスト pass ============
3
4
# CI で同じコマンド
5
$ pytest tests/
6
ModuleNotFoundError: No module named 'analyzers'

ローカル mac でしか再現しない、CI Linux 環境だけ落ちる。

原因の1つ目: conftest.py の sys.path.insert

tests/conftest.py
1
import sys
2
from pathlib import Path
3
4
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

これがあると tests/ 配下から from analyzers import ... が通ります。しかし このコードが評価されるかどうか は、pytest の起動位置や pyproject.toml の設定に依存します。

1
src/
2
analyzers/
3
__init__.py
4
tests/
5
conftest.py ← sys.path.insert を持つ
6
unit/
7
conftest.py ← もう一つ別の path.insert を持つ
8
test_x.py

複数の conftest.py が散在し、それぞれが 微妙に違う path を sys.path に入れる。pytest は 1 番上から順に評価しますが、CI 環境とローカルで cwd が違うと、評価順が変わって動くこともあれば動かないこともある状態に。

原因の2つ目: 相対 import が混ざる

tests/unit/test_aggregator.py
1
from ..helpers import make_dummy # 相対 import
2
3
# tests/integration/test_pipeline.py
4
from analyzers import aggregator # 絶対 import

両方が混在していると、pytest の rootdir がどこかで解釈ミスを起こすと一気に全部 import エラーになります。

解決方針

  1. 絶対 import に統一する(相対 import を禁止)
  2. conftest.py の sys.path 操作を禁止する
  3. pyproject.tomlpythonpath を一元管理する
  4. CI で grep ベースで禁則違反を検出する

pyproject.toml で pythonpath を 1 か所に集約

1
[tool.pytest.ini_options]
2
pythonpath = ["src"]
3
testpaths = ["tests"]

これで pytest 起動時に常に src/ が sys.path に入ります。conftest.py で個別に操作する必要がなくなります。

conftest.py を最小化

tests/conftest.py
1
"""共通フィクスチャだけを置く。sys.path 操作はここで一切しない。"""
2
import pytest
3
4
@pytest.fixture
5
def sample_records():
6
return [...]

syspathlib.Path も import しません。何もしていない conftest.py が正しい姿です。

相対 import を禁止する

from .helpers import ... のような相対 import は、ファイル移動時に壊れたり、IDE のリファクタリングが追えなかったりします。絶対 import に統一します。

1
# Bad: 相対
2
from ..helpers import make_dummy
3
4
# Good: 絶対
5
from tests.helpers import make_dummy

tests/helpers.py を作って、そこから絶対パスで import します。

CI で禁則を検知する

人間の規律だけでは戻ります。grep ベースの check を CI に入れました。

禁則 1: conftest.py での sys.path.insert

scripts/check_no_sys_path_in_conftest.sh
1
#!/bin/bash
2
set -e
3
4
if grep -rn "sys.path" --include="conftest.py" .; then
5
echo "ERROR: conftest.py で sys.path 操作を行わないでください"
6
echo "→ pyproject.toml の [tool.pytest.ini_options] pythonpath を使う"
7
exit 1
8
fi
9
echo "OK: conftest.py に sys.path 操作なし"

禁則 2: テストファイルでの相対 import

scripts/check_no_relative_imports.sh
1
#!/bin/bash
2
set -e
3
4
# tests/ 配下の .py で `from .` または `from ..` を検索
5
if grep -rEn "^from \.+ " --include="*.py" tests/; then
6
echo "ERROR: tests/ 配下では相対 import を使わないでください"
7
echo "→ from tests.helpers import ... のように絶対 import で書く"
8
exit 1
9
fi
10
echo "OK: 相対 import なし"

禁則 3: テストファイル冒頭の sys.path 操作

conftest.py だけでなく テストファイル本体で sys.path をいじるパターンも見かけます。これも止めます。

Terminal window
1
if grep -rEn "^sys\.path\.(insert|append)" --include="*.py" tests/; then
2
echo "ERROR: テストファイルで sys.path 操作を行わないでください"
3
exit 1
4
fi

CI への組み込み

.github/workflows/ci.yml
1
- name: Check import hygiene
2
run: |
3
bash scripts/check_no_sys_path_in_conftest.sh
4
bash scripts/check_no_relative_imports.sh

pytest 実行前にこれが落ちると、import 構造の問題が 実際の test 失敗より先に検知できます。エラーメッセージで「修正方法」までセットで出すと、レビュー時に説明する手間が減ります。

移行作業

1. conftest.py の整理

リポジトリ全体で conftest.py を grep して、sys.path を含むものを順に削除しました。

Terminal window
1
grep -l "sys.path" $(find . -name "conftest.py")

各ファイルの sys.path.insert を削除し、pyproject.tomlpythonpath で代替できることを確認します。

2. 相対 import の絶対化

Terminal window
1
# 検出
2
grep -rEn "^from \.+ " --include="*.py" tests/

地道に書き換えます。from .helpers import xfrom tests.helpers import x

3. テスト全体の再実行

CI で完全にクリーンな環境で pytest を回し、全部通るかを確認します。

Terminal window
1
# クリーン環境のシミュレーション
2
docker run --rm -v $(pwd):/app -w /app python:3.11 \
3
bash -c "pip install -e . && pytest tests/"

ローカルの「たまたま通っていた」依存関係を排除して検証できます。

学び

1. 「ローカルで通る」は信用できない

conftest.py が複数ある、sys.path を編集している、相対 import が混ざっている、のどれかがある時点で 環境依存の不具合が潜んでいます。pytest は柔軟すぎるので、明示的な制約を入れたほうが堅牢になります。

2. pyproject.toml の pythonpath は強力

pythonpath 1 行で sys.path 操作を全部不要にできます。プロジェクト全体の import の起点が明文化されるので、新しいメンバーが「どこから import するか」で迷わない メリットも大きいです。

3. grep ベースの CI チェックは費用対効果が高い

完全な静的解析より雑ですが、grep で 90% は捕まえられます。実装も 10 行のシェルスクリプトで済みます。正規の lint ツールが対応していない独自規約を強制するときに便利です。

4. 「動いている」状態を 1 度壊す勇気

「conftest.py の sys.path を消したら全部壊れるんじゃ」と思って先延ばしにしていましたが、実際にやってみると 2 時間で完了しました。先延ばしのほうが負債を増やしていました。

まとめ

観点BeforeAfter
pythonpath 管理conftest.py に分散pyproject.toml に一元化
import スタイル相対と絶対が混在絶対のみ
sys.path 操作テストごとに散在ゼロ
CI 検知なしgrep ベースで禁則を強制
ローカル/CI の差異頻発解消

CI で謎の ModuleNotFoundError が出るたびに対症療法していると、いつまでも根が抜けません。

pyproject.tomlpythonpath + 絶対 import 統一 + grep ベース CI、この 3 つを揃えると import 関連の不具合がほぼ消えました。「pytest が柔軟すぎて困る」と感じたら、自分たちで明示的な制約を入れるのが解決策でした。

Article title:conftest.py の sys.path.insert に依存したテストを CI で透視する:絶対 import 統一と grep 禁則
Article author:45395
Release time:2026-05-05

記事へのご質問・ご感想をお聞かせください

フィードバックを送る