![[Pytest] pytest-xdistの高レベルスコープフィクスチャでの意図しない挙動と対処方法](https://images.ctfassets.net/ct0aopd36mqt/wp-thumbnail-a873a00568e5285b2051ff6c5aae2c3d/febef60aecfe576a837a7efcb96625eb/python_icatch.png)
[Pytest] pytest-xdistの高レベルスコープフィクスチャでの意図しない挙動と対処方法
こんにちは。サービス開発室の武田です。
担当しているプロジェクトでテスト実行時間の短縮に取り組んだ際、pytest-xdistを導入しました。その中で特有の問題に遭遇し、scope="module"
フィクスチャの動作について深く理解する必要がありました。その調査結果と対処法を紹介します。
scope="module"
フィクスチャの予期しない動作
pytest-xdistを導入した直後、今まで通っていたテストがランダムで失敗するようになりました。エラーメッセージを見ると、テストが期待したディレクトリではない場所で実行される状況が頻発していました。
# 失敗するテスト例
def test_module_functionality():
current_dir = os.getcwd()
# 期待: /path/to/project/src/module_a
# 実際: /tmp/test_module_b_xyz123 (別のテストのディレクトリ)
assert "module_a" in current_dir # -> AssertionError
調査した結果、問題の根本原因はscope="module"フィクスチャの並列実行での動作にありました。
scope="module"
フィクスチャとpytest-xdistの関係
通常のpytestでの動作(期待される動作)
まずは問題となったテストコードです。
@pytest.fixture(scope="module", autouse=True)
def setup_module():
original_cwd = os.getcwd()
os.chdir("src/module_a")
yield
os.chdir(original_cwd) # モジュール終了時に復元
このコードは順次実行では次のように動作します。
TestModuleA実行開始
→ setup_module (A) → ディレクトリ変更
→ TestA1, TestA2実行
→ teardown_module (A) → ディレクトリ復元
TestModuleB実行開始
→ setup_module (B) → ディレクトリ変更
→ TestB1, TestB2実行
→ teardown_module (B) → ディレクトリ復元
各モジュールが独立して実行され、フィクスチャも期待どおりに動作します。
pytest-xdistでの動作(実際の動作)
しかし、pytest-xdistではワーカープロセス単位でフィクスチャが管理されます。
Worker gw0:
TestModuleB → setup_module (B) → ディレクトリ変更 → TestB1実行
TestModuleA → (setup_module (A) が実行されず)TestA1が間違ったディレクトリで実行 → 失敗
この動作を理解するには、次の仕様を把握する必要があります。
scope="module"
は「モジュール単位で1回実行」を意味する- しかしpytest-xdistでは「ワーカープロセス単位で管理」される
- 複数のテストモジュールが同じワーカーで実行される際、先にロードされたmoduleフィクスチャの状態が残る
公式ドキュメントでの説明
この動作は、pytest-xdistの公式制限として明記されています(引用元は英語)。
「各ワーカープロセスは独自のcollectionを実行し、すべてのテストのサブセットを実行します。これは、異なるプロセスで高レベルスコープのフィクスチャ(例:session)を要求するテストがフィクスチャコードを1回以上実行することを意味し、期待を破り、特定の状況では望ましくない場合があります。」
参考: How-tos
また、GitHub Issueでは次のような回答がありました。
「各プロセスは独自のセッションです...これを実装するよい健全で簡単な方法はありません」
参考: Session-Scoped Fixtures are not Session-Scoped with Pytest-Xdist #271
実際に修正したコミット
実際のプロジェクトでは、この問題の影響を受けたテストは複数あり、それぞれ修正をしました。
# 修正前(問題のあるコード)
@pytest.fixture(scope="module", autouse=True)
def scope_module():
cwd = os.getcwd()
os.chdir("src/module_a")
yield
os.chdir(cwd)
# 修正後(解決済みコード)
@pytest.fixture(autouse=True) # scope="function"がデフォルト
def scope_function(monkeypatch):
monkeypatch.chdir("src/module_a")
解決策とその理由
scope="function"
+ monkeypatchの使用
1.次のような利点があります。
- 自動復元: テスト終了時や例外時に確実に元の状態に戻る
- pytestネイティブ: pytestが提供する標準的な機能
- 独立実行: 各テストが独立した環境で実行
2. --dist=loadscope オプション(部分的解決)
公式ドキュメントによると、次のオプションで同一モジュールのテストを同一ワーカーに集約できます(引用元は英語)。
pytest -n auto --dist=loadscope
「テストは、テスト関数の場合はモジュールごとに、テストメソッドの場合はクラスごとにグループ化されます。これにより、グループ内のすべてのテストが同じプロセスで実行されることが保証されます。これは、コストの高いモジュールレベルまたはクラスレベルのフィクスチャがある場合に有用です。」
3. 根本的な設計見直し
推奨アプローチ:
- フィクスチャで永続的な状態変更を避ける
- 各テストの独立性を重視
- pytestの標準ツール(monkeypatch等)を積極的に使用
まとめ
pytest-xdistの並列実行では、pytestのフィクスチャスコープが期待どおりに動作しない場合があります。特に高レベルスコープ(scope="session"
、scope="module"
)のフィクスチャは、ワーカープロセス単位で管理されるため、次の問題を引き起こす可能性があります。
- moduleスコープ: 状態変更の汚染(今回の事例)
- sessionスコープ: リソース重複実行やポート競合
この問題は公式の制限として明記されており、適切な回避策を講じる必要があります。monkeypatchなどのpytest標準ツールを活用することで、より堅牢で保守性の高いテストスイートを構築できます。
並列テスト実行は開発効率向上に大きく貢献しますが、テストの独立性と品質に、より一層注意を払う必要があります。参考になれば幸いです。