【第3回】Python pytest 入門 — fixtureとモックを組み合わせた実践的なテスト設計

プログラミング

前回は pytest の parametrize と基本的な fixture の書き方を紹介しました。今回はその続きで、conftest.py を使った fixture の共有と、モックとの組み合わせ方について整理していきます。

個人的にここが pytest の「急に世界が広がる」ポイントだと思っていて、理解すると一気にテストが書きやすくなります。ただ最初はちょっと概念が多くて混乱しがちなので、自分が詰まったところを中心にまとめてみます。

この記事でわかること

  • conftest.py の役割と fixture の自動検出の仕組み
  • fixture のスコープ設定による実行タイミングの制御
  • yield を使ったセットアップ・ティアダウンの書き方
  • pytest-mock を使ったモック fixture の定義と組み合わせ方
  • 実際のプロジェクトでの conftest.py の階層設計

conftest.py とは何か

fixture を複数のテストファイルで使い回したいとき、都度コピーするのは当然つらい。そこで登場するのが conftest.py です。

仕組みはシンプルで、pytest が自動的にこのファイルを読み込んでくれるため、明示的に import しなくても fixture が使えるようになります。

ディレクトリ構成のイメージはこんな感じ:

project/
├── src/
│   └── myapp.py
└── tests/
    ├── conftest.py        # ← ここに共通 fixture を置く
    ├── test_users.py
    └── test_orders.py

conftest.py の fixture は、pytest から見て見える範囲のテストから import なしで参照できるようになります。一般的には、同じディレクトリ配下のテストで共有する目的で置くことが多いです。さらにネストして置くことも可能で、tests/unit/conftest.pytests/conftest.py を使い分けるようなことも普通にやります。

注意点としては、conftest.py は import して使うものではなく、pytest が自動検出するものなので、他のモジュールから from conftest import xxx のようなことはしないほうが無難です。テスト用パッケージ配下に配置し、他所からインポートしない運用が推奨されています。

fixture のスコープを使いこなす

fixture には scope という引数があって、「どの単位で使い回すか」を制御できます。

  • function(デフォルト): テスト関数ごとに毎回実行
  • class: テストクラスごとに1回
  • module: テストファイルごとに1回
  • package: パッケージ(ディレクトリ)ごとに1回
  • session: テスト実行全体で1回

たとえばデータベース接続のような重い処理は session スコープにしておくと、毎テストで接続が走らなくて済みます。逆に、テスト間でデータが汚染されると困るものは function のままにしておくのが安全です。

import pytest

# セッション全体で1回だけ実行
@pytest.fixture(scope="session")
def db_connection():
    conn = create_db_connection()
    yield conn
    conn.close()

# テストファイルごとに1回
@pytest.fixture(scope="module")
def sample_users():
    return [
        {"id": 1, "name": "Alice"},
        {"id": 2, "name": "Bob"},
    ]

スコープはむやみに広げないほうが良いです。fixture の設定をテスト間で共有すると依存関係が生まれてしまい、不意にテストが成功してしまうケースがあるからです。自分は最初 session を多用しすぎて痛い目を見ました。

yield を使ったセットアップ・ティアダウン

fixture の中で yield を使うと、テスト前後の処理(セットアップとティアダウン)を1つの関数にまとめられます。

import pytest

@pytest.fixture
def temp_file(tmp_path):
    filepath = tmp_path / "test.txt"
    filepath.write_text("hello")
    yield filepath          # ← テスト関数に渡る
    filepath.unlink(missing_ok=True)  # テスト後に削除

yield より前がセットアップ、後ろがティアダウンです。return だとティアダウンが書けないので、後片付けが必要な場合は yield が便利です。

余談ですが、pytest の公式ドキュメントでは yield スタイルが推奨されている形式で、addfinalizer を使った複数のファイナライザー関数を直接登録するやり方も存在します。後者は複雑なクリーンアップが必要なケース向けの選択肢として説明されることが多いです。

モックと fixture を組み合わせる

ここが今回のメインテーマです。外部 API やデータベースへのアクセスをテストから切り離すためにモックを使いますが、それを fixture として定義することで、複数のテストで使い回せるようになります。

モックには大きく2つの選択肢があります:

  • 標準ライブラリの unittest.mock.patch
  • pytest-mock プラグインの mocker fixture

pytest-mockpip install pytest-mock で入ります。インストールすると mocker という fixture が使えるようになります。差し替えたい処理を patch で指定してモックにします。後片付けまで含めて扱いやすいので便利です。

mocker を使った基本パターン

import pytest

# conftest.py に書いておけば全テストで使い回せる
@pytest.fixture
def mock_api_client(mocker):
    mock = mocker.patch("myapp.services.APIClient")
    mock.return_value.get_data.return_value = {"status": "ok", "data": [1, 2, 3]}
    return mock
# test_service.py
def test_fetch_data_success(mock_api_client):
    from myapp.services import fetch_data
    result = fetch_data()
    assert result["status"] == "ok"
    mock_api_client.return_value.get_data.assert_called_once()

自分の理解だと、pytest-mockmocker.patch を使った場合は、テストの終了時にパッチが元に戻るように自動的に扱えることが多いです。なので、状況によっては patcher.stop() を毎回意識しなくて済むのが地味に助かります。

fixture を fixture に渡す

pytest では、fixture の引数に別の fixture を指定することができます。複数のモックオブジェクトを使う場合にも、テストの引数に mocker や fixture を追加するだけで済みます。

import pytest
from myapp.services import UserService

@pytest.fixture
def mock_db(mocker):
    return mocker.patch("myapp.services.db_client")

@pytest.fixture
def user_service(mock_db):  # ← mock_db を注入
    mock_db.return_value.find_user.return_value = {"id": 1, "name": "Alice"}
    return UserService()

def test_get_user(user_service):
    user = user_service.get_user(1)
    assert user["name"] == "Alice"

fixture がコンポーザブルな設計になっていて、大きなサービスクラスのテストでも整理して書けます。オブジェクト指向っぽい感じがしますね。

import の仕方でモックのパスが変わる

モックを使うときにハマりがちなポイントとして、モックするパスは「使われる側」の名前空間で指定するという決まりがあります。

# services.py
from myapp.db import db_client   # ← from ... import 形式で取り込んでいる場合

# この場合のモックパスは↓
mocker.patch("myapp.services.db_client")   # ✅ 正しい
mocker.patch("myapp.db.db_client")         # ❌ これだと効かない

import osfrom os import getcwd ではモックのパスの指定方法が変わります。最初これを知らなくて「モックしてるのになんで呼ばれてるんだ」と30分くらい悩みました。わりと多くの人が同じところで詰まるみたいです。

autouse で全テストに自動適用する

autouse=True を設定した fixture を conftest.py に定義することで、テスト用ディレクトリ内で定義されたすべてのテストの実行前に前処理を自動実施できます。

# conftest.py
import pytest

@pytest.fixture(autouse=True)
def reset_env(monkeypatch):
    # 全テストで環境変数をリセットする
    monkeypatch.setenv("APP_ENV", "test")
    monkeypatch.setenv("DEBUG", "false")

環境変数の初期化とか、テスト用フラグの設定みたいな「毎回やりたいけど書くのが面倒」な処理に向いています。ただ、使いすぎると何が起きてるかわかりにくくなるので、本当に全テストで必要なものに絞るのが無難かなと思います。

実際のプロジェクトでどう整理するか

最後に、自分が今使っているざっくりとした構成を紹介します。

tests/
├── conftest.py          # セッション・モジュールスコープの共通 fixture
├── unit/
│   ├── conftest.py      # unit テスト専用の fixture(モック中心)
│   ├── test_users.py
│   └── test_orders.py
└── integration/
    ├── conftest.py      # DB接続など重い fixture(scopeを広めに)
    └── test_db.py

conftest.py を階層ごとに分けることで、「unit テストにしか関係ない fixture」が integration 側に漏れるのを防げます。pytest の conftest.py の探索はディレクトリ構造に沿って行われるので、意図した通りに動かせることが多いです。

正直、この構成が最善かどうかはまだよくわかってないです。プロジェクトの規模によっても変わるし、もっとうまいやり方がある気がします。

※この記事にはプロモーションが含まれます

ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー

まとめ

conftest.py と fixture の組み合わせを理解することで、テストコード全体をかなり整理できるようになります。ポイントをおさらいすると:

  • conftest.py は pytest が自動検出するファイル。明示的に import しない
  • fixture のスコープで実行タイミングを制御。むやみに広げないことが大事
  • yield を使うことで前処理・後処理を1つにまとめられる
  • pytest-mock と組み合わせることで、モック fixture も簡潔に書ける
  • モックのパスは「使われる側」の名前空間で指定する。import の形によって変わる点は注意
  • autouse=True で全テストに自動適用できるが、本当に必要なもの限定で
  • conftest.py を階層分けすることで、スコープを意図的に制御できる

📚 シリーズ「Python pytest 入門」(第3回 / 全4回)

← 前回の記事: 前回の記事はこちら

→ 次回の記事: 公開後にリンクが追加されます

参考になったらクリックしてもらえると嬉しいです!

Blogmura ProgrammingProgramming Ranking
タイトルとURLをコピーしました