Claude Media
Claude Codeでpytestのparametrizeとfixtureを整理する手順

Claude Codeでpytestのparametrizeとfixtureを整理する手順

似た形のテスト関数をparametrizeにまとめ、conftest.pyのfixtureのスコープと置き場所を見直す整理を、Claude Codeに任せるときの指示と、前後でテスト数と結果が変わっていないことの確かめ方を扱います。

pytestのテストコードは、機能を足すたびに「コピーして値だけ変えた関数」と「とりあえずconftest.pyに置いたfixture」が積もります。これをClaude Codeに整理させるときの要点は、整理の前後でテストの件数と結果が変わっていないことを、指示の段階から検証手順として渡すことです。以下では、小さなサンプルを使って、parametrizeへの集約、fixtureのスコープ見直し、conftest.pyの分割を順に進めます。

整理の前に決めておく「変えてはいけないもの」

整理はテストの意味を変えない作業です。ところがテストコードは、壊れても気づきにくい性質があります。parametrizeの値リストを写し間違えると、テストは緑のまま件数だけが減ります。fixtureのスコープを広げると、テスト同士が状態を共有して、順序によって結果が変わります。

そこでClaude Codeに依頼する前に、次の3点を「変えてはいけないもの」として固定します。

  • 収集されるテストの件数(整理前後で同じ)
  • 各ケースの結果(すべて同じ成否)
  • fixtureが作られる回数(減るのは意図した変更として確認する)

この3点は、いずれもpytestの標準オプションで取れます。件数は--collect-only -q、結果は-rA、fixtureの実行計画は--setup-planまたは--setup-showです。--collect-only -qで収集結果をファイルに書き出して加工する使い方は、公式の使い方ページにも載っています。

整理前のサンプルを用意する

説明用に、送料を計算する関数と、その5つのテストを用意しました。pytest 9.1.1を空の仮想環境に入れて実行しています。

# shipping.py
def fee(weight_kg, region):
    base = {"jp": 500, "us": 1500, "eu": 1200}[region]
    return base + max(0, weight_kg - 1) * 100

整理前のconftest.pyは、次のように育ってしまった状態を想定します。

# tests/conftest.py(整理前)
import pytest
 
CALLS = {"rates": 0}
 
@pytest.fixture
def rates():
    CALLS["rates"] += 1
    return {"jp": 500, "us": 1500, "eu": 1200}
 
@pytest.fixture
def unused_user():
    return {"name": "a"}
 
@pytest.fixture
def heavy_parcel():
    return 5
 
@pytest.fixture(scope="session", autouse=True)
def report_calls():
    yield
    print(f"rates fixture built {CALLS['rates']} times")

ratesは読み取り専用の定数を返すだけなのに、既定のfunctionスコープでテストごとに作り直されています。unused_userはどのテストからも使われていません。heavy_parcelは1つのテストファイルでしか使われないのに、全体共有のconftest.pyに置かれています。report_callsは、ratesが何回作られたかを数えるために足した計測用です。

# tests/test_fee.py(整理前)
from shipping import fee
 
def test_fee_jp_light(rates):
    assert fee(1, "jp") == rates["jp"]
 
def test_fee_us_light(rates):
    assert fee(1, "us") == rates["us"]
 
def test_fee_eu_light(rates):
    assert fee(1, "eu") == rates["eu"]
 
def test_fee_jp_heavy(rates, heavy_parcel):
    assert fee(heavy_parcel, "jp") == rates["jp"] + 400
 
def test_fee_us_heavy(rates, heavy_parcel):
    assert fee(heavy_parcel, "us") == rates["us"] + 400

基準を取る

Claude Codeに触らせる前に、基準値を記録します。次のコマンドで、件数と結果とfixtureの作成回数が分かります。

pytest -q -s -rA
pytest --collect-only -q

手元の実行では、5件がすべて通り、計測用の出力はrates fixture built 5 timesでした。収集結果はtests/test_fee.py::test_fee_jp_lightのような5行です。この出力を、そのまま整理後と見比べる基準にします。

似た関数をparametrizeにまとめる

5つの関数は、重さ・地域・加算額の組み合わせだけが違います。pytestの@pytest.mark.parametrizeは、1つのテスト関数に複数の引数セットを与える標準の仕組みです。公式の例のように、pytest.paramで個別のケースにidやmarksを付けることもできます。

Claude Codeには、機械的に写せる範囲を明示して頼みます。たとえば次のような依頼です。

tests/test_fee.py の5つの関数を、@pytest.mark.parametrize 1つにまとめてください。
- 各ケースは pytest.param の id で元の関数名の違い(jp-light など)を残す
- 値リストは元のテストから1件ずつ写し、増減させない
- まとめたあと pytest --collect-only -q を実行し、件数が5件のままか報告する

依頼文に書くことは、「どの関数をまとめるか」「ケースの識別子をどう残すか」「件数の確認をいつ行うか」の3つです。ここでClaude Codeが何を出力するかは実行するまで分かりませんが、整理後の形は次のようになります(例示です)。

# tests/test_fee.py(整理後の例)
import pytest
 
from shipping import fee
 
@pytest.mark.parametrize(
    "weight, region, surcharge",
    [
        pytest.param(1, "jp", 0, id="jp-light"),
        pytest.param(1, "us", 0, id="us-light"),
        pytest.param(1, "eu", 0, id="eu-light"),
        pytest.param(5, "jp", 400, id="jp-heavy"),
        pytest.param(5, "us", 400, id="us-heavy"),
    ],
)
def test_fee(rates, weight, region, surcharge):
    assert fee(weight, region) == rates[region] + surcharge

heavy_parcelのfixtureは、値5を引数に直接書いたことで不要になりました。

件数の数え間違いを防ぐ

parametrizeは、デコレータを積むと組み合わせが掛け算になります。公式の例では、xに2値、yに2値を積むと4通りで実行されます。「5件の独立したケース」を「5×3件の直積」にしてしまうと、件数が増える方向に崩れます。逆に、値リストが空になるケースは、empty_parameter_set_markオプションで挙動が決まります。動的にリストを作っている場合は、空になっていないかを件数で確かめます。

前後の差を取る

整理後に、基準と同じ2つのコマンドを実行します。

pytest -q -s -rA
pytest --collect-only -q

手元の結果は次のとおりです。

項目整理前整理後
収集件数整理前5件整理後5件
結果整理前5 passed整理後5 passed
ノードID整理前test_fee_jp_lightなど5関数整理後test_fee[jp-light]など1関数の5ケース
ratesの作成回数整理前5回整理後1回

ノードIDの文字列は、関数がまとまったので一致しません。比べるのは件数と成否です。id=で元の関数名の違いを残しておくと、-rAの一覧で整理前の各テストと1対1に対応づけられます。

落とし穴

緑のまま件数だけ減る

ノードIDの一致を確認しようとして、依頼文に「出力が完全に同じこと」と書くと、parametrizeを使った時点で必ず食い違います。確かめる対象は件数、成否、idから元の関数への対応の3つに絞ります。件数が5件から4件に減ったときは、緑のまま1ケースが消えています。

fixtureのスコープを見直す

次に、ratesのスコープです。pytestのfixtureは既定ではテストごとに作られ、scopeにfunction、class、module、package、sessionを指定できます。公式ガイドは、ネットワーク接続のように作成が高価なfixtureでscope="module"を使うと、モジュール内のテスト関数が同じインスタンスを受け取って時間を節約できると説明しています。

ratesは読み取り専用の辞書を返すだけなので、sessionに広げても挙動は変わりません。conftest.pyを次のように書き換えます。

# tests/conftest.py(整理後の例)
import pytest
 
CALLS = {"rates": 0}
 
@pytest.fixture(scope="session")
def rates():
    CALLS["rates"] += 1
    return {"jp": 500, "us": 1500, "eu": 1200}
 
@pytest.fixture(scope="session", autouse=True)
def report_calls():
    yield
    print(f"rates fixture built {CALLS['rates']} times")

unused_userとheavy_parcelは消えています。手元の実行ではrates fixture built 1 timesになり、5回あった作成が1回に減りました。

実行計画で確かめる

スコープを変えたときは、--setup-planでfixtureの実行計画を見ます。整理後の出力にはSETUP S ratesのように、スコープを表す1文字(Sはsession、Fはfunction)が付きます。--setup-showなら、実際の実行中の準備と後片付けが見えます。

Claude Codeへの依頼は、スコープを変える根拠まで書かせると安全です。

tests/conftest.py の rates fixture を session スコープに広げてください。
- テストが rates を書き換えていないことを grep で確認し、結果を報告する
- 広げたあと pytest --setup-plan -q を実行し、rates の SETUP が S になっているか示す

広げてはいけないfixture

スコープを広げると、テスト間でオブジェクトが共有されます。公式ガイドの例では、scope="module"のfixtureを2つのテストが受け取ると、トレースバックに「まったく同じオブジェクト」が渡されたことが表示されます。可変のオブジェクト(リスト、辞書、DB接続の状態)を広いスコープで共有すると、あるテストの変更が次のテストに残ります。

目安は次のとおりです。

fixtureの中身スコープの方向
読み取り専用の定数・設定スコープの方向広げてよい
作成が高価で、テストが変更しない接続スコープの方向広げる候補
テストが書き込むオブジェクトスコープの方向functionのまま
後片付けが必要なものスコープの方向yieldで片付けを書き、スコープに合わせる

公式ガイドは、後片付けにはfixture内でyieldを使う方法を推奨しています。parametrizeされたfixtureには注意点もあります。pytestはfixtureのインスタンスを同時に1つしかキャッシュしないため、パラメータ付きのfixtureは、同じスコープの中で複数回呼ばれることがあります。

conftest.pyの置き場所を直す

conftest.pyに置いたfixtureは、そのディレクトリ配下のテストからインポートなしで使えます。ディレクトリごとに別のconftest.pyを置けて、親のfixtureに加わる形になります。探索は上向きで、テストは自分より内側のディレクトリにあるfixtureを見つけられません。同名のfixtureが複数あるときは、最初に見つかったものが使われます。

この性質から、肥大化したconftest.pyは次の方針で分けられます。

  • 1つのテストファイルでしか使わないfixtureは、そのテストファイルに移す
  • 特定のサブディレクトリでしか使わないfixtureは、そのディレクトリのconftest.pyに移す
  • 全体で使うものだけをtests直下のconftest.pyに残す

使われていないfixtureを探すには、まずpytest --fixturesで定義の一覧と場所(unused_user -- tests/conftest.py:11の形式)を出し、fixture名をgrepで数えます。引数名での使用だけでなく、autouseやusefixtures経由の使用もあるので、autouse=Trueのfixtureは名前の参照がなくても生きています。pytest --fixturesの一覧では、名前が_で始まるfixtureは-vを付けたときだけ表示されます。

落とし穴

同名fixtureの上書きに気づかない

最初に見つかったfixtureが使われる以上、移動で同名のfixtureが2つできると、近い側が親の定義を上書きします。移動の前後でpytest --fixturesの一覧を比べ、同じ名前が2か所に出ていないかを確認します。

Claude Code側の設定に落とす

整理は一度で終わらず、新しいテストを足すたびにconftest.pyは太ります。Claude Codeには、整理後の方針をルールとして残します。公式は、CLAUDE.mdを検証できる粒度で書くこと、1ファイル200行未満に収めること、一部のファイルにしか関係しない内容はパススコープ付きのルールに移すことを勧めています。テスト整理の方針は後者に向いています。

---
paths:
  - "tests/**/*.py"
---
 
# テストの整理方針
 
- 値だけが違う関数は @pytest.mark.parametrize にまとめ、pytest.param の id を付ける
- fixture は使うテストファイルに置く。複数ファイルで使うものだけ conftest.py に置く
- scope は読み取り専用のものだけ広げる。広げたら pytest --setup-plan -q で確認する
- 整理の前後で pytest --collect-only -q の件数と pytest -q の結果を比べて報告する

このファイルを.claude/rules/の下に置くと、tests/配下のファイルをRead、Write、Editで扱うときに読み込まれます。テスト以外の作業では文脈を消費しません。pytestのCLAUDE.mdへの書き方全般はClaude CodeにpytestのCLAUDE.md規約を教えるにまとめています。

失敗が出たときの切り分けは別の手順になります。整理で緑が赤に変わった場合は、pytestの失敗をMCPサーバーでClaudeに解析させるが参考になります。

整理の対象を絞る

Claude Codeに「テスト全体を整理して」と頼むと、変更が広がって前後比較も難しくなります。1回の依頼は、1つのテストファイルと、それが使うfixtureまでに絞ります。直近の変更全体を整えたいなら、code-simplifierプラグインで直近の変更を整理する手順もありますが、どの経路で整理しても、件数と結果の比較は同じコマンドで行えます。

Webフレームワークのテストなら、test_clientを使った判定の組み立てをClaude CodeでFlaskアプリを開発するで扱っています。

まとめ

整理の成否は、変更後の見た目ではなく、前後の件数と結果の一致で決まります。parametrizeの値リストの写し間違いは緑のまま件数が減る形で現れ、fixtureのスコープ拡大は共有による順序依存として現れます。どちらも、基準の採取と--collect-only -q、-rA、--setup-planの3点で機械的に捕まえられます。方針はtests/**/*.pyに限定したルールとして残しておくと、次の整理でも同じ確認手順が再利用されます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.pytest.org/en/stable/how-to/fixtures.html
  2. docs.pytest.org/en/stable/how-to/parametrize.html
  3. docs.pytest.org/en/stable/reference/fixtures.html
  4. docs.pytest.org/en/stable/how-to/usage.html
  5. code.claude.com/docs/en/memory
この記事を共有:XはてブLinkedIn