pytest の @pytest.mark.parametrize は、1つのテスト関数を複数パターンで実行できる強力な機能です。API テストの認証パターン切り替えや、UI テストのロール別アクセス権限検証など、実務では欠かせない場面が多くあります。pytest.param・indirect・fixture params との使い分けまで理解すると、データ駆動テストの幅が大きく広がります。
📌 この記事の対象読者
✅ @pytest.mark.parametrize の基本は知っているが、もっと使いこなしたい方 |
✅ pytest.param の id・marks の使い方を知りたい方 |
✅ indirect を使った fixture との連携方法を学びたい方 |
✅ fixture params との使い分けを整理したい方 |
📖 この記事を読むとわかること
| ✔ 複数引数・複数デコレーターによるパラメータの組み合わせ方 |
✔ pytest.param で id・marks(skip・xfail 含む)を付けて管理性を高める方法 |
✔ indirect=True で fixture とパラメータを連携させる方法 |
✔ fixture params との使い分け判断基準(実務頻度つき比較表) |
| ✔ 組み合わせ爆発への対処法(ペアワイズ・境界値・リスクベース) |
✔ pytest-xdist との組み合わせで並列実行を高速化する方法 |
筆者はQAエンジニアとして15年以上、API テスト・機能テストの自動化に携わってきました。
「認証トークンを複数パターンで切り替えながら API テストを回す」「ロール別のアクセス権限を一括検証する」といった場面でパラメータ化テストを多用してきた経験から、実際の現場での失敗例と改善パターンを交えて解説します。
✅ この記事の結論
pytest.param(id=..., marks=...)でパターンに名前と属性を付けて管理性を上げるindirect=Trueでパラメータを fixture に渡し、セットアップも一緒に切り替える- 単一テストへの入力バリエーションには
parametrize、複数テストへの共通適用には fixtureparamsを使う
@pytest.mark.parametrize の基本(復習)
応用パターンに入る前に、最小構成を確認しておきます。
import pytest
# 引数名とパターンリストを指定するだけ
@pytest.mark.parametrize("value, expected", [
(1, True),
(0, False),
(-1, False),
])
def test_is_positive(value: int, expected: bool) -> None:
assert (value > 0) == expected
このテストは3パターン分実行されます。出力は test_is_positive[1-True] のように自動で名前が付きます。
応用パターン 5選
1. pytest.param で id・marks を付けて管理しやすくする
パターンが増えると「どのケースが失敗したか」が分かりにくくなります。
pytest.param で id(テスト名)と marks(属性) を付けることで一気に見通しが良くなります。
import pytest
import requests
@pytest.mark.parametrize("payload, expected_status", [
pytest.param(
{"username": "admin", "password": "correct"},
200,
id="valid_credentials",
),
pytest.param(
{"username": "admin", "password": "wrong"},
401,
id="wrong_password",
),
# smoke マークで絞り込み実行(pytest -m smoke)が可能
pytest.param(
{"username": "", "password": ""},
400,
id="empty_credentials",
marks=pytest.mark.smoke,
),
# 既知バグのため一時スキップ
pytest.param(
{"username": "admin", "password": None},
400,
id="null_password",
marks=pytest.mark.skip(reason="Issue #123 対応待ち"),
),
# 将来修正予定の失敗パターン(xfail)
pytest.param(
{"username": " ", "password": "pass"},
400,
id="whitespace_username",
marks=pytest.mark.xfail(reason="空白のみのユーザー名が現在通過してしまう"),
),
])
def test_login(payload: dict, expected_status: int) -> None:
response = requests.post(
"https://example.com/api/auth/login", # サンプルURL
json=payload,
timeout=(3, 10),
)
assert response.status_code == expected_status
id を付けると test_login[valid_credentials] のように表示されます。
id なしでは test_login[payload0-200] のような自動生成名になり、パターン数が増えると特定が難しくなります。
| marks の種類 | 用途 | 実務での使いどころ |
|---|---|---|
pytest.mark.smoke | カスタム分類 | CI で基本確認だけ先に実行したいとき |
pytest.mark.skip | 一時スキップ | 既知バグ・環境依存のパターンを除外 |
pytest.mark.xfail | 失敗が期待される | 修正予定のバグをテストに残しておきたいとき |
pytest.mark.smoke などは pytest.ini(または pyproject.toml)に登録しないと PytestUnknownMarkWarning が出ます。skip・xfail は組み込みマークなので登録不要です。# pytest.ini
[pytest]
markers =
smoke: スモークテスト(基本動作確認)
slow: 実行時間が長いテスト2. 複数の parametrize を積み重ねて組み合わせを作る
@pytest.mark.parametrize を複数積み重ねると、全パターンの直積(組み合わせ)が生成されます。
import pytest
# 2 × 3 = 6パターン自動生成
@pytest.mark.parametrize("lang", ["ja", "en", "ko"])
@pytest.mark.parametrize("role", ["admin", "user"])
def test_dashboard_access(lang: str, role: str) -> None:
# 実務例: 言語 × ロールの全組み合わせでアクセス権限を検証
assert lang in ("ja", "en", "ko")
assert role in ("admin", "user")
積み重ねるとパターン数が掛け算で増えます(3 × 3 × 3 = 27、4 × 4 × 4 = 64)。実行時間が急増するため、以下の方法でパターンを絞りましょう。
- 境界値分析:最小値・最大値・境界±1 のみ選ぶ
- ペアワイズテスト:任意の2因子の組み合わせが必ず1回は網羅されるように絞る(
allpairspy等のツールが使えます) - リスクベース選択:障害発生時の影響が大きい組み合わせを優先し、低リスクの組み合わせを間引く
- pytest.param + skip:不要な組み合わせだけ
marks=pytest.mark.skipで除外する
3. indirect=True で fixture にパラメータを渡す
pytest fixture 入門ガイド|yield・scope・conftest.pyの書き方を実務視点で解説
indirect=True を使うと、パラメータの値がそのままテスト関数に渡るのではなく、同名の fixture に渡されてからテスト関数に注入されます。
セットアップ処理をパラメータに応じて切り替えたいときに便利です。
import pytest
import requests
# パラメータを受け取って「認証済みセッション」を返す fixture
@pytest.fixture
def authed_session(request: pytest.FixtureRequest) -> requests.Session:
role: str = request.param # ← parametrize の値がここに入る
credentials = {
"admin": {"username": "admin_user", "password": "admin_pass"},
"user": {"username": "normal_user", "password": "user_pass"},
}
cred = credentials[role]
session = requests.Session()
res = session.post(
"https://example.com/api/auth/login", # サンプルURL
json=cred,
timeout=(3, 10),
)
res.raise_for_status()
session.headers.update({"Authorization": f"Bearer {res.json()['token']}"})
yield session
session.close()
# indirect=True で authed_session fixture にパラメータを渡す
@pytest.mark.parametrize("authed_session", ["admin", "user"], indirect=True)
def test_profile_access(authed_session: requests.Session) -> None:
res = authed_session.get(
"https://example.com/api/profile", # サンプルURL
timeout=(3, 10),
)
assert res.status_code == 200
indirect=True を使わない場合、"admin" という文字列がそのままテスト関数に渡されます。
fixture を経由することでログイン処理・後片付けも自動化できるのがポイントです。
4. 一部だけ indirect にする(部分 indirect)
複数引数のうち特定の引数だけを indirect にすることもできます。
import pytest
import requests
@pytest.fixture
def authed_session(request: pytest.FixtureRequest) -> requests.Session:
role: str = request.param
session = requests.Session()
# ... ログイン処理 ...
yield session
session.close()
# authed_session だけ indirect、endpoint はそのままテストに渡す
@pytest.mark.parametrize(
"authed_session, endpoint",
[
pytest.param("admin", "/api/admin/users", id="admin_users"),
pytest.param("admin", "/api/admin/reports", id="admin_reports"),
pytest.param("user", "/api/profile", id="user_profile"),
],
indirect=["authed_session"], # ← リストで対象引数を指定
)
def test_endpoint_access(authed_session: requests.Session, endpoint: str) -> None:
res = authed_session.get(
f"https://example.com{endpoint}", # サンプルURL
timeout=(3, 10),
)
assert res.status_code == 200
5. pytest-xdist と組み合わせて並列実行を高速化する
pytest-xdist を使うと、parametrize で生成された複数パターンを並列で実行できます。
パターン数が多いテストスイートの実行時間を大幅に短縮できます。
pip install pytest-xdist
# CPUコア数に合わせて自動並列化
pytest -n auto
# コア数を指定する場合
pytest -n 4
pytest-xdist で並列実行する場合、状態を保持する fixture をパラメータ化しているとテスト間で干渉が起きる可能性があります。特に DB への書き込みや共有リソースへのアクセスを含む fixture は注意が必要です。
読み取り専用のデータや独立したリソースを扱う fixture であれば、session・module スコープでも並列実行で問題が起きにくくなります。
parametrize と fixture params の使い分け
どちらも「複数パターンでテストを繰り返す」ための機能ですが、得意領域が異なります。
まず fixture params の書き方を確認しておきましょう。
fixture params のコード例
import pytest
import requests
# fixture の params でロールを定義
@pytest.fixture(params=[
pytest.param("admin", id="admin_role"),
pytest.param("editor", id="editor_role"),
pytest.param("viewer", id="viewer_role"),
])
def role(request: pytest.FixtureRequest) -> str:
return request.param
# この fixture を使う全テストが自動で3ロール分実行される
def test_can_access_dashboard(role: str) -> None:
res = requests.get(
f"https://example.com/api/dashboard", # サンプルURL
headers={"X-Role": role},
timeout=(3, 10),
)
assert res.status_code == 200
def test_can_view_profile(role: str) -> None:
res = requests.get(
f"https://example.com/api/profile", # サンプルURL
headers={"X-Role": role},
timeout=(3, 10),
)
assert res.status_code == 200
parametrize と異なり、role fixture を引数に持つすべてのテスト関数が自動で3パターン実行されます。
各テスト関数にデコレーターを書く必要がないのがポイントです。
比較表
| 比較項目 | @pytest.mark.parametrize | fixture params | 実務頻度 |
|---|---|---|---|
| 書く場所 | テスト関数のデコレーター | fixture 定義内 | — |
| 単一テストへの入力バリエーション | ◎ | △ | ★★★ |
| 複数テスト関数への共通パターン適用 | △(関数ごとに記載が必要) | ◎ | ★★☆ |
| セットアップ処理もパラメータ化 | △(indirect が必要) | ◎(自然に書ける) | ★★☆ |
| 組み合わせテスト(直積) | ◎(デコレーター積み重ね) | △ | ★★☆ |
| id・marks によるパターン管理 | ◎(pytest.param) | ◎(pytest.param) | ★★★ |
| 初心者への読みやすさ | ◎ | △ | — |
判断フロー
| 1つのテスト関数に複数の入力パターンを渡したい | → @pytest.mark.parametrize |
| 複数のテスト関数に同じパターンを使い回したい | → fixture params |
| パラメータに応じてセットアップも変えたい | → fixture params または indirect |
| パラメータの組み合わせ(直積)を自動生成したい | → @pytest.mark.parametrize 複数重ね |
テストデータを別ファイルで管理して共有する
複数のテストファイルで同じパラメータリストを使い回す場合、専用のテストデータファイル(例: tests/test_data.py)にまとめると管理しやすくなります。
conftest.py から直接 import する方法は技術的に動くこともありますが、実行ディレクトリへの依存や ModuleNotFoundError の原因になりやすいため、パラメータデータは専用ファイルに分離する方が安全です。
# ディレクトリ構成
tests/
├── conftest.py ← fixture のみ(パラメータデータは置かない)
├── test_data.py ← パラメータデータを一元管理
├── test_login.py
└── test_users.py
# tests/test_data.py
import pytest
VALID_USER_PAYLOADS = [
pytest.param({"username": "admin", "password": "admin123"}, id="admin"),
pytest.param({"username": "editor", "password": "edit456"}, id="editor"),
pytest.param({"username": "viewer", "password": "view789"}, id="viewer"),
]
INVALID_USER_PAYLOADS = [
pytest.param({"username": "", "password": "pass"}, id="empty_username"),
pytest.param({"username": "admin", "password": ""}, id="empty_password"),
pytest.param({}, id="empty_body"),
]
# tests/test_login.py
import pytest
import requests
from tests.test_data import VALID_USER_PAYLOADS, INVALID_USER_PAYLOADS
@pytest.mark.parametrize("payload", VALID_USER_PAYLOADS)
def test_login_success(payload: dict) -> None:
res = requests.post(
"https://example.com/api/auth/login", # サンプルURL
json=payload,
timeout=(3, 10),
)
assert res.status_code == 200
@pytest.mark.parametrize("payload", INVALID_USER_PAYLOADS)
def test_login_failure(payload: dict) -> None:
res = requests.post(
"https://example.com/api/auth/login", # サンプルURL
json=payload,
timeout=(3, 10),
)
assert res.status_code in (400, 422)
パラメータリストを変数として分離するだけで、テスト関数側は見通しがよくなり、パターン追加も1箇所の修正で完了します。
Playwright でのパラメータ化例
Playwright でも同じ書き方でパラメータ化テストが使えます。
ログイン検証をブラウザテストで回す場合の例です。
import pytest
from playwright.sync_api import Page
@pytest.mark.parametrize("username, password, expected_url", [
pytest.param("admin", "correct", "/dashboard", id="admin_login"),
pytest.param("user", "correct", "/home", id="user_login"),
pytest.param("admin", "wrong", "/login", id="invalid_password"),
pytest.param("", "", "/login", id="empty_credentials"),
])
def test_login_redirect(
page: Page,
username: str,
password: str,
expected_url: str,
) -> None:
page.goto("https://example.com/login") # サンプルURL
page.fill("[data-testid='username']", username)
page.fill("[data-testid='password']", password)
page.click("[data-testid='submit']")
page.wait_for_url(f"**{expected_url}")
assert expected_url in page.url
page fixture は pytest-playwright が提供する組み込み fixture です。
@pytest.mark.parametrize と組み合わせることで、ブラウザを再起動せずに複数パターンを効率よく検証できます。
実務でよくはまるポイント 7選
⚠️ pytest parametrize ではまりやすいポイント
- テスト関数名に
test_プレフィックスがない
pytest はデフォルトでtest_で始まる関数のみ収集します。パラメータ化しても関数名が間違っていると一切実行されません。 - conftest.py の置き場所が間違っている
共有 fixture を conftest.py に書く場合、テストファイルと同じか上位ディレクトリに置く必要があります。別ディレクトリでは認識されません。 - 状態を保持する fixture をパラメータ化している
DB への書き込みや共有リソースへのアクセスを含む fixture をパラメータ化すると、pytest-xdistなどで並列実行したときにテスト間で干渉が起きる場合があります。読み取り専用・独立リソースであれば session・module スコープでも問題が起きにくくなります。 - カスタムマークを pytest.ini に登録していない
pytest.param(..., marks=pytest.mark.smoke)を使う場合、pytest.ini(またはpyproject.toml)へのマーク登録が必要です。skip・xfailは組み込みマークなので登録不要です。 - print() の出力が見えない
pytest はデフォルトで標準出力をキャプチャします。パラメータごとの出力を確認したいときはpytest -sかloggingモジュールを使いましょう。 - セットアップ中の例外で ERROR になる
fixture や indirect 処理内で例外が発生すると FAILED ではなく ERROR と表示されます。パラメータのデータ型や想定外の値を渡していないか確認しましょう。 - ルートディレクトリ以外から実行して ModuleNotFoundError が出る
from tests.test_data import ...などのパッケージ import は、プロジェクトルートから実行しないとパスが解決できなくなります。pytest.iniがあるルートから実行してください。
よくある質問(FAQ)
id は必須ですか? 必須ではありませんが、5パターン以上になったら付けることを推奨します。
id がないと test_login[payload0-200] のような自動生成名になり、CI のログで失敗パターンを特定しづらくなります。
最初から付ける習慣にしておくと後から困りません。
複数のテスト関数に同じパターンを自動適用したい場合は fixture params が自然です。
特定のテスト関数だけパラメータに応じてセットアップを変えたい場合は indirect が向いています。
どちらも「パラメータを fixture に渡す」点は同じですが、適用範囲と記述場所が異なります。迷ったら上の比較表の判断フローを参考にしてください。
indirect=True と indirect=["引数名"] の違いは? indirect=True はすべての引数を fixture 経由にします。indirect=["authed_session"] のようにリストで指定すると、特定の引数だけを fixture 経由にできます。複数引数のうち一部はそのままテストに渡したい場合はリスト形式を使ってください。
pytest.param(..., marks=pytest.mark.skip(reason="...")) でそのパターンだけスキップできます。
また pytest.mark.xfail を使うと「失敗が期待されるパターン」として記録できます。
テスト全体を止めずに問題のあるパターンを一時的に除外したいときに便利です。
CSV・JSON・YAML ファイルからパラメータを読み込んで渡す方法があります。
また、テスト生成を動的に制御できる pytest_generate_tests フックを使うと、条件に応じてパラメータを絞り込む高度な制御も可能です(中級者向け)。
まずはどのパターンが本当に必要かを見直し、境界値・代表値・異常値の代表パターンに絞ることが先決です。
@pytest.mark.parametrize はクラスベースのテストでも使えますか? 使えます。クラスのメソッドに付けることも、クラス全体に付けることも可能です。
クラス全体に付けると、クラス内のすべてのテストメソッドが同じパラメータセットで実行されます。
まとめ
| ポイント | 内容 |
|---|---|
pytest.param で id・marks | パターンに名前と属性を付けて管理性・可読性を高める。skip・xfail も活用 |
| 複数デコレーターで直積 | 組み合わせを自動生成できる。爆発対策はペアワイズ・境界値・リスクベースで |
indirect=True | パラメータを fixture に渡してセットアップも切り替える |
| 部分 indirect | リストで指定して一部の引数だけ fixture 経由にする |
| pytest-xdist との組み合わせ | 複数パターンを並列実行して高速化。状態干渉のある fixture には注意 |
| test_data.py で共有 | conftest.py ではなく専用ファイルに分離して安全に import する |
| fixture params との使い分け | 単一テストへの入力変化 → parametrize、複数テストへの共通適用 → fixture params |
@pytest.mark.parametrize の応用パターンを使いこなすと、テストケースの増加をコード量の増加なく実現できます。
まずは既存のテストに pytest.param で id を付けるところから始めてみてください。
パターン管理が整うと、後からケースを追加・削除する作業が格段に楽になります。

