テストコードで毎回同じ前準備や後片付けを書くのは手間がかかります。
pytestのフィクスチャを使うと、前処理と後処理を1か所にまとめ、テストからは簡潔に再利用できます。
本記事では、yield
による前後処理、scope
とautouse
、conftest.py
の共有方法、そして初心者がつまずきやすい注意点まで、実用コードと実行結果つきで詳しく解説します。
pytestのフィクスチャとは?
フィクスチャは、テスト関数が使う共通の準備物や資源を提供し、必要ならテスト後の後片付けも担う仕組みです。
テスト関数の引数にフィクスチャ名を書くと、自動的にその戻り値や状態が注入されます。
この依存性注入の考え方により、テスト本体はロジックの検証に集中でき、準備と片付けは外出しで再利用できます。
前処理/後処理を共通化するメリット
フィクスチャで前後処理を共通化する最大の利点は、コードの重複とミスを減らせることです。
例えば一時ディレクトリの作成と削除、環境変数の設定と復元、ログ設定の初期化とリセットなど、毎回書くと冗長で壊れやすい処理を1か所に集約できます。
テストが増えるほど保守コストの差は大きく、不具合の混入も防げます。
setup/teardownより読みやすい
従来のunittest
スタイルではsetUp
/tearDown
やsetup_method
/teardown_method
のようなxUnitのフックを使いますが、pytestのフィクスチャはテスト関数の引数として明示的に依存関係が見えるので、どのテストが何を使っているかが読み取りやすいです。
また、yield
により1つの関数で前後処理を完結でき、複数のフィクスチャを柔軟に合成できます。
用語のキホン
用語 / 概念 | 説明 |
---|---|
フィクスチャ | @pytest.fixture で定義する前準備と後片付けの単位 |
依存性注入 | テスト関数の引数にフィクスチャ名を書くと、pytestが自動で値を渡す |
yield フィクスチャ | yield の前が前処理、後が後処理 |
scope | フィクスチャの生存期間。function 、module 、session などがある |
autouse | 明示的に引数に書かなくても自動で適用するかどうかを指定 |
conftest.py | ディレクトリ単位でフィクスチャを共有する設定ファイル |
最小のpytestフィクスチャ例
ここでは最小の例から、yield
での前後処理、一時ファイルの扱い、フィクスチャの合成までを段階的に示します。
yieldで前後処理を1つにまとめる
yield
を使うと1つのフィクスチャ関数内に前処理と後処理を自然に書けます。
標準出力にフックの順序が見えるようにして実行します。
# ファイル名: 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
は特に頻出です。
# ファイル名: 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
フィクスチャ同士を組み合わせて再利用
フィクスチャは引数に他のフィクスチャを取ることで合成できます。
基本部品を組み合わせて、より高レベルな準備処理を構築できます。
# ファイル名: 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種に慣れるのがおすすめです。
スコープの動作が分かる最小例
以下では標準出力にログを出して動きを可視化します。
# ファイル名: 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
にすると、テスト関数の引数に書かなくてもフィクスチャが自動適用されます。
全テストに共通の初期化を強制したい場合に便利です。
# ファイル名: 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
にフィクスチャを置くと、そのディレクトリ配下の全テストからインポート無しで使えます。
プロジェクト共通の土台に適しています。
project/
tests/
conftest.py
test_a.py
test_b.py
# ファイル名: 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)}
# ファイル名: tests/test_a.py
def test_a_uses_config(app_config):
assert "mode" in open(app_config["file"]).read()
# ファイル名: 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
を併用すると安全です。
# ファイル名: 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です。
# ファイル名: 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
のレベルとハンドラをテストごとに初期化し、終了時に元に戻す例です。
# ファイル名: 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
パターンを使い、後処理を確実に書くのが安全です。
# 悪い例: 後処理がないためファイルが閉じられない
import pytest
@pytest.fixture
def bad_file(tmp_path):
f = (tmp_path / "x.txt").open("w")
f.write("x")
return f # 後処理なし
# 良い例: yieldの後で確実にクローズ
import pytest
@pytest.fixture
def good_file(tmp_path):
f = (tmp_path / "x.txt").open("w")
yield f
f.close()
代替として、動的にクリーンアップを積み上げる必要がある場合はrequest.addfinalizer
も使えます。
# 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
などの組み込みフィクスチャを活用しつつ、必要に応じてフィクスチャを合成すれば、読みやすく壊れにくいテストが書けます。
さらにscope
とautouse
の設定、conftest.py
での共有により、高速かつ一貫性のあるテスト基盤を築けます。
最後にyield忘れや名前重複、過度なautouseといった落とし穴を避け、まずはfunction
スコープを基本に小さく始めるのがおすすめです。
これらを押さえることで、Python初心者でも確かなテスト運用に一歩近づけます。