【第2回】Python pytest 入門 — patch・MagicMock・side_effectを使いこなすモック応用術

プログラミング

前回は pytest の基本的な書き方——テスト関数の作り方、assert の使い方、pytest.raises で例外をテストする方法を紹介しました。今回は「フィクスチャ(fixture)」を使ったテストの整理術をまとめます。

フィクスチャって名前だけ聞くとちょっと仰々しいんですが、要は「テストの前後処理をまとめておく仕組み」です。これを知ってからテストの書き方がだいぶ変わった気がします。

この記事でわかること

  • pytest フィクスチャの基本的な書き方
  • yield を使ったセットアップ&ティアダウン
  • scope でフィクスチャの生存期間を制御する方法
  • conftest.py でフィクスチャを複数ファイルで共有する
  • フィクスチャ間の依存関係の作り方
  • autouse で全テストに自動適用する
  • pytest 標準のよく使われるフィクスチャ

フィクスチャとは何か

テストを書いていると「この setup 処理、全テストで同じこと書いてるな……」ってなることが多いです。たとえば毎回ダミーのユーザーデータを作ったり、DB接続を準備したり。

フィクスチャはそれを一箇所にまとめて、必要なテストにだけ渡せる仕組みです。Pythonの関数として定義して、@pytest.fixture デコレータをつけるだけで使えます。

import pytest

@pytest.fixture
def user_data():
    return {"name": "ニャンチュー", "age": 28, "plan": "free"}

def test_user_name(user_data):
    assert user_data["name"] == "ニャンチュー"

def test_user_plan(user_data):
    assert user_data["plan"] == "free"

テスト関数の引数名とフィクスチャ名を一致させるだけで、pytest が自動的に渡してくれます。import も何もいらない。この「引数名で解決する」設計がすごく好きで、DI(依存性の注入)っぽい雰囲気があります。

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

フィクスチャの中でよく使うのが yield を使ったパターンです。yield の前がセットアップ、後がティアダウン(後片付け)になります。

import pytest

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

def test_read_file(tmp_file):
    assert tmp_file.read_text() == "hello"

ポイントは、「yield まで到達していれば」テストが失敗しても yield 以降のティアダウンコードは実行されるところです。ただし yield の前に例外が起きて yield できなかった場合は、ティアダウンは走りません。try/finally を自分で書かなくていい。

余談ですが、最初は returnyield の使い分けで少し迷いました。後片付けが不要なら return、あるなら yield、という理解でだいたい合ってると思います。

スコープ(scope)でフィクスチャの生存期間を制御する

フィクスチャには scope という引数があって、「このフィクスチャをどの単位で使い回すか」を指定できます。

  • function(デフォルト): テスト関数ごとに実行
  • class: テストクラスごとに1回
  • module: ファイル(モジュール)ごとに1回
  • session: テストセッション全体で1回
import pytest

@pytest.fixture(scope="module")
def db_connection():
    print("\n[setup] DB接続")
    conn = {"status": "connected"}   # 実際はDB接続処理
    yield conn
    print("\n[teardown] DB切断")
    conn["status"] = "disconnected"

def test_query_1(db_connection):
    assert db_connection["status"] == "connected"

def test_query_2(db_connection):
    assert db_connection["status"] == "connected"

scope="module" にすると、同じファイル内のテストでは DB接続が1回だけ走ります。重い初期化処理をテストのたびに繰り返したくないときに重宝します。

ただし、スコープを広げると副作用が漏れやすくなるというトレードオフもあります。あるテストが共有データをうっかり書き換えてしまうと、後続のテストが通ったり落ちたりするようになります。基本は function スコープで書いて、本当に遅くてどうにもならないときだけ広げるのがよさそうです。

conftest.py でフィクスチャを共有する

フィクスチャをテストファイルをまたいで使いたい場合は conftest.py に書きます。pytest が自動で読み込んでくれるので、import する必要もないです。

# tests/conftest.py
import pytest

@pytest.fixture
def base_config():
    return {
        "env": "test",
        "debug": True,
        "api_url": "http://localhost:8000",
    }
# tests/test_api.py
def test_api_url(base_config):
    assert base_config["api_url"].startswith("http")

# tests/test_config.py
def test_env(base_config):
    assert base_config["env"] == "test"

ディレクトリ構造が入れ子になっている場合、親ディレクトリの conftest.py から子ディレクトリのテストへとフィクスチャが引き継がれます。サブモジュールだけに使いたいフィクスチャは、そのディレクトリの conftest.py に書けばスコープを絞れます。

実際のプロジェクトだと tests/conftest.py に共通フィクスチャをまとめて、機能ごとのサブディレクトリにさらに conftest.py を置くパターンをよく見ます。最初はファイル1個で十分だと思いますが。

フィクスチャの中でフィクスチャを使う

フィクスチャは別のフィクスチャに依存させることができます。複雑なセットアップを段階的に組み立てるときに便利です。

import pytest

@pytest.fixture
def user():
    return {"id": 1, "name": "ニャンチュー"}

@pytest.fixture
def authenticated_client(user):
    # user フィクスチャを受け取って、認証済みクライアントを作る
    return {"user_id": user["id"], "token": "dummy_token_abc"}

def test_authenticated_request(authenticated_client):
    assert authenticated_client["token"] == "dummy_token_abc"
    assert authenticated_client["user_id"] == 1

pytest がフィクスチャの依存グラフを解決して、必要な順番で実行してくれます。自分でインスタンスを渡し回さなくていいのがラクです。

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

autouse=True をつけると、明示的に引数に書かなくてもすべてのテストに自動でフィクスチャが適用されます。

import pytest
import os

@pytest.fixture(autouse=True)
def reset_env():
    original = os.environ.copy()
    yield
    # テスト後に環境変数を元に戻す
    os.environ.clear()
    os.environ.update(original)

環境変数の初期化や、ログのリセット、テスト間の状態汚染を防ぎたいときに使います。ただ autouse は「なんで動いてるかわからない」という状況になりやすいので、乱用は禁物です。conftest.py に書いて、コメントで意図を残しておくのが無難かなと思います。

組み込みフィクスチャも地味に便利

pytest 自身がいくつかの便利なフィクスチャを標準で提供しています。自分がよく使うのはこのあたり:

  • tmp_path: テストごとに一時ディレクトリを作ってくれる。ファイル操作のテストで重宝する
  • monkeypatch: 環境変数や関数を一時的に差し替えるやつ
  • capsys: 標準出力をキャプチャする。print() ベースのコードをテストするときに
  • caplog: ログ出力をキャプチャする
def greet(name):
    print(f"Hello, {name}!")

def test_greet_output(capsys):
    greet("ニャンチュー")
    captured = capsys.readouterr()
    assert captured.out == "Hello, ニャンチュー!\n"

tmp_path は特に好きで、テスト用の一時ファイルを作るときに os.makedirs とかを自分で書かなくてよくなります。テスト終了後に自動削除されるのも助かります。

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

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

まとめ

フィクスチャを使うとテストコードの見通しがかなりよくなります。最初は「わざわざデコレータつけて別関数に切り出すの面倒では?」と思っていたんですが、テストが増えてくると確実に効いてきます。

個人的に抑えておくといいと思う順番は、@pytest.fixture の基本 → yield によるティアダウン → conftest.py での共有、くらいです。scopeautouse はその後でも間に合います。

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

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

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

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

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