閉じる

【Pythonテスト効率化】 pytestフィクスチャで前後処理を共通化

テストコードで毎回同じ前準備や後片付けを書くのは手間がかかります。

pytestのフィクスチャを使うと、前処理と後処理を1か所にまとめ、テストからは簡潔に再利用できます

本記事では、yieldによる前後処理、scopeautouseconftest.pyの共有方法、そして初心者がつまずきやすい注意点まで、実用コードと実行結果つきで詳しく解説します。

pytestのフィクスチャとは?

フィクスチャは、テスト関数が使う共通の準備物や資源を提供し、必要ならテスト後の後片付けも担う仕組みです。

テスト関数の引数にフィクスチャ名を書くと、自動的にその戻り値や状態が注入されます。

この依存性注入の考え方により、テスト本体はロジックの検証に集中でき、準備と片付けは外出しで再利用できます。

前処理/後処理を共通化するメリット

フィクスチャで前後処理を共通化する最大の利点は、コードの重複とミスを減らせることです。

例えば一時ディレクトリの作成と削除、環境変数の設定と復元、ログ設定の初期化とリセットなど、毎回書くと冗長で壊れやすい処理を1か所に集約できます。

テストが増えるほど保守コストの差は大きく、不具合の混入も防げます。

setup/teardownより読みやすい

従来のunittestスタイルではsetUp/tearDownsetup_method/teardown_methodのようなxUnitのフックを使いますが、pytestのフィクスチャはテスト関数の引数として明示的に依存関係が見えるので、どのテストが何を使っているかが読み取りやすいです。

また、yieldにより1つの関数で前後処理を完結でき、複数のフィクスチャを柔軟に合成できます。

用語のキホン

用語 / 概念説明
フィクスチャ@pytest.fixtureで定義する前準備と後片付けの単位
依存性注入テスト関数の引数にフィクスチャ名を書くと、pytestが自動で値を渡す
yieldフィクスチャyieldの前が前処理、後が後処理
scopeフィクスチャの生存期間。functionmodulesessionなどがある
autouse明示的に引数に書かなくても自動で適用するかどうかを指定
conftest.pyディレクトリ単位でフィクスチャを共有する設定ファイル

最小のpytestフィクスチャ例

ここでは最小の例から、yieldでの前後処理、一時ファイルの扱い、フィクスチャの合成までを段階的に示します。

yieldで前後処理を1つにまとめる

yieldを使うと1つのフィクスチャ関数内に前処理と後処理を自然に書けます。

標準出力にフックの順序が見えるようにして実行します。

Python
# ファイル名: test_yield_fixture.py
import pytest

@pytest.fixture
def sample_resource():
    # 前処理: リソースを用意する
    print("setup: open resource")
    resource = {"answer": 42}
    # yieldの値がテスト側に渡される
    yield resource
    # 後処理: リソースを解放する
    print("teardown: close resource")

def test_use_resource(sample_resource):
    # フィクスチャの戻り値が引数で受け取れる
    assert sample_resource["answer"] == 42
実行結果
$ pytest -s -q test_yield_fixture.py
setup: open resource
.
teardown: close resource
1 passed in 0.02s

tmp_pathフィクスチャで一時ファイルを扱う

pytestには便利な組み込みフィクスチャが多数あります。

一時ディレクトリを提供するtmp_pathは特に頻出です。

Python
# ファイル名: test_tmp_path.py
# 一時ファイルを作成し、内容を読み書きする例です。
def test_write_and_read(tmp_path):
    data_file = tmp_path / "hello.txt"
    # テキストを書き出す
    data_file.write_text("pytest makes testing easy")
    # ファイルが存在するか検証
    assert data_file.exists()
    # 内容の検証
    content = data_file.read_text()
    assert content == "pytest makes testing easy"
実行結果
$ pytest -q test_tmp_path.py
.
1 passed in 0.01s

フィクスチャ同士を組み合わせて再利用

フィクスチャは引数に他のフィクスチャを取ることで合成できます。

基本部品を組み合わせて、より高レベルな準備処理を構築できます。

Python
# ファイル名: test_composed_fixtures.py
import json
import pytest

@pytest.fixture
def data_dir(tmp_path):
    # tmp_pathにサンプルデータを配置する
    dir_ = tmp_path / "data"
    dir_.mkdir()
    (dir_ / "config.json").write_text(json.dumps({"mode": "dev", "retry": 2}))
    return dir_

@pytest.fixture
def load_config(data_dir):
    # data_dirに依存して設定ファイルを読み込む高レベルフィクスチャ
    cfg_path = data_dir / "config.json"
    with cfg_path.open() as f:
        return json.load(f)

def test_config_loaded(load_config):
    # 合成フィクスチャから最終的な値だけを受け取れる
    assert load_config["mode"] == "dev"
    assert load_config["retry"] == 2
実行結果
$ pytest -q test_composed_fixtures.py
.
1 passed in 0.01s

pytestフィクスチャのscopeとautouseの使い方

フィクスチャの寿命と自動適用の設定は、テスト速度と可読性のバランスに直結します。

ここを正しく設計すると、テストの実行時間短縮と安定性向上に効きます。

scope(function/module/session)の選び方

スコープはフィクスチャの生成と破棄のタイミングを決めます。

以下の表は代表的な3種類の使い分けです。

スコープ生成タイミング破棄タイミング典型用途注意点
function各テスト関数の実行前そのテスト終了時一時ファイル、環境変数、毎回初期化したい状態最も安全だが負荷が高い処理には向かない
moduleモジュール内の最初のテスト前モジュール内の最後のテスト後使い捨てのDBスキーマやHTTPサーバなどテスト間の状態汚染に注意
sessionテストセッション開始時全テスト終了時高価な接続や大きなテストデータの読み込み並列実行時の共有資源に注意

補足としてclassスコープもありますが、まずは上記3種に慣れるのがおすすめです。

スコープの動作が分かる最小例

以下では標準出力にログを出して動きを可視化します。

Python
# ファイル名: test_scope_demo.py
import pytest

@pytest.fixture(scope="session")
def session_res():
    print("session: setup")
    yield "S"
    print("session: teardown")

@pytest.fixture(scope="module")
def module_res():
    print("module: setup")
    yield "M"
    print("module: teardown")

@pytest.fixture(scope="function")
def function_res():
    print("function: setup")
    yield "F"
    print("function: teardown")

def test_one(session_res, module_res, function_res):
    assert session_res + module_res + function_res == "SMF"

def test_two(session_res, module_res, function_res):
    assert session_res == "S"
実行結果
$ pytest -s -q test_scope_demo.py
session: setup
module: setup
function: setup
.
function: teardown
function: setup
.
function: teardown
module: teardown
session: teardown
2 passed in 0.03s

autouse=Trueで共通前処理を自動化

autouse=Trueにすると、テスト関数の引数に書かなくてもフィクスチャが自動適用されます。

全テストに共通の初期化を強制したい場合に便利です。

Python
# ファイル名: test_autouse_seed.py
import random
import pytest

@pytest.fixture(autouse=True)
def fixed_seed():
    # 毎テストで疑似乱数のシードを固定
    random.seed(0)

def test_random_is_deterministic():
    # 固定シードにより最初の値は常に同じ
    assert random.random() == 0.8444218515250481

def test_random_next_is_also_deterministic():
    # 各テストでseed(0)されるので、再び同じ最初の値が得られる
    assert random.random() == 0.8444218515250481
実行結果
$ pytest -q test_autouse_seed.py
..
2 passed in 0.01s
注意

autouseは便利ですが、テストからは見えにくくなります。

影響範囲が広い初期化はコメントやドキュメントで明示し、必要最小限に留めると読みやすさを保てます。

conftest.pyで全テストに共有

conftest.pyにフィクスチャを置くと、そのディレクトリ配下の全テストからインポート無しで使えます。

プロジェクト共通の土台に適しています。

text
project/
  tests/
    conftest.py
    test_a.py
    test_b.py
Python
# ファイル名: tests/conftest.py
import pytest

@pytest.fixture
def app_config(tmp_path):
    # 共通の設定ディレクトリとファイルを用意
    cfg_dir = tmp_path / "config"
    cfg_dir.mkdir()
    cfg_file = cfg_dir / "app.ini"
    cfg_file.write_text("mode=dev\nretries=3\n")
    return {"dir": str(cfg_dir), "file": str(cfg_file)}
Python
# ファイル名: tests/test_a.py
def test_a_uses_config(app_config):
    assert "mode" in open(app_config["file"]).read()
Python
# ファイル名: tests/test_b.py
def test_b_sees_same_structure(app_config):
    # tmp_pathは各テストで別ディレクトリなので相互に汚染しません
    assert app_config["file"].endswith("app.ini")
実行結果
$ pytest -q tests
..
2 passed in 0.02s

初心者向けの実用パターンと注意点

日常のテストで役立つパターンを、前後処理とともに紹介します。

環境変数の前後処理

環境変数はプロセス全体に影響するため、テストごとに必ず復元する必要があります。

monkeypatchを併用すると安全です。

Python
# ファイル名: test_env_fixture.py
import os
import pytest

@pytest.fixture
def set_app_mode(monkeypatch):
    # 前処理: テスト用の環境変数をセット
    monkeypatch.setenv("APP_MODE", "test")
    yield
    # 後処理: APP_MODEを削除または元に戻す
    monkeypatch.delenv("APP_MODE", raising=False)

def get_mode():
    # 本番コード想定の小関数
    return os.getenv("APP_MODE", "prod")

def test_mode_is_test(set_app_mode):
    assert get_mode() == "test"

def test_mode_defaults_to_prod():
    # フィクスチャ未使用のテストでは既定値
    assert get_mode() == "prod"
実行結果
$ pytest -q test_env_fixture.py
..
2 passed in 0.01s
補足

monkeypatch無しで実装する場合はos.environ.copy()でスナップショットを取り、最後に復元する方法でもOKです。

Python
# ファイル名: test_env_snapshot.py
import os
import pytest

@pytest.fixture
def env_snapshot():
    before = os.environ.copy()
    yield
    # 差分を反映して厳密に復元する
    os.environ.clear()
    os.environ.update(before)

def test_env_with_snapshot(env_snapshot):
    os.environ["X"] = "1"
    assert os.getenv("X") == "1"
# テスト終了時にXは確実に消えます

ログや設定の初期化/リセット

ログ設定はプロセス全体で共有されがちです。

以下は、loggingのレベルとハンドラをテストごとに初期化し、終了時に元に戻す例です。

Python
# ファイル名: test_logging_reset.py
import logging
import pytest

@pytest.fixture
def reset_logging():
    # 前処理: 現在の状態を保存し、INFOで初期化
    root = logging.getLogger()
    prev_level = root.level
    prev_handlers = root.handlers[:]
    for h in root.handlers[:]:
        root.removeHandler(h)
    logging.basicConfig(level=logging.INFO)
    yield
    # 後処理: ハンドラとレベルを復元
    root = logging.getLogger()
    for h in root.handlers[:]:
        root.removeHandler(h)
    for h in prev_handlers:
        root.addHandler(h)
    root.setLevel(prev_level)

def test_logging_output(reset_logging, caplog):
    # caplogでINFO以上を捕捉
    caplog.set_level(logging.INFO)
    logger = logging.getLogger("demo")
    logger.info("hello fixture")
    assert "hello fixture" in caplog.text
実行結果
$ pytest -q test_logging_reset.py
.
1 passed in 0.01s

よくあるミス(yield忘れ/名前重複)の防止

1. yieldを忘れて後処理が実行されない ファイルや接続のクローズを書き忘れると、リソースリークやテスト順序依存が発生します。

基本はyieldパターンを使い、後処理を確実に書くのが安全です。

Python
# 悪い例: 後処理がないためファイルが閉じられない
import pytest

@pytest.fixture
def bad_file(tmp_path):
    f = (tmp_path / "x.txt").open("w")
    f.write("x")
    return f  # 後処理なし
Python
# 良い例: yieldの後で確実にクローズ
import pytest

@pytest.fixture
def good_file(tmp_path):
    f = (tmp_path / "x.txt").open("w")
    yield f
    f.close()

代替として、動的にクリーンアップを積み上げる必要がある場合はrequest.addfinalizerも使えます。

Python
# addfinalizerの例
import pytest

@pytest.fixture
def resource(request):
    res = object()
    def cleanup():
        print("cleanup called")
    request.addfinalizer(cleanup)
    return res

2. フィクスチャ名の重複 組み込みフィクスチャ名と同名のフィクスチャを定義すると意図せず上書きします。

例えばtmp_pathという自作フィクスチャは避けてください。

プロジェクト固有の接頭辞を付けると衝突を防げます(e.g. app_tmp_path)

3. 過度なautouse autouse=Trueを濫用すると、テストから挙動が読み取りづらくなります。

プロジェクトの基本契約として必須の初期化だけに限定しましょう。

4. スコープの過大化 高速化を狙ってsessionにし過ぎると、状態の持ち越しによるテスト干渉が起きます。

まずはfunctionをデフォルトにし、計測の上で必要な箇所のみ広げる方が安全です。

まとめ

pytestのフィクスチャは、テストの前準備と後片付けを安全かつ再利用可能にする最強の道具です。

yieldで前後処理を1つにまとめ、tmp_pathなどの組み込みフィクスチャを活用しつつ、必要に応じてフィクスチャを合成すれば、読みやすく壊れにくいテストが書けます。

さらにscopeautouseの設定、conftest.pyでの共有により、高速かつ一貫性のあるテスト基盤を築けます。

最後にyield忘れや名前重複、過度なautouseといった落とし穴を避け、まずはfunctionスコープを基本に小さく始めるのがおすすめです。

これらを押さえることで、Python初心者でも確かなテスト運用に一歩近づけます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!