pytest parametrizeは、同じテストを異なるデータで繰り返し実行するための仕組みです。基本の書き方はすぐ覚えられる一方で、「失敗したテストの名前が読みにくい」「特定のケースだけスキップしたい」「fixtureにデータを渡したい」といった場面で手が止まりがちです。この記事では、ids、pytest.param、indirect、複数の重ね掛け、Playwrightでの使い方、外部データの読み込みを、実務で起きやすい失敗例と合わせて整理します。
最大のポイントは「parametrizeは1ケースが1テストになる。だから、名前と管理のしやすさが保守性を左右する」という点です。データを並べるだけで終わらせず、idsとpytest.paramで1件ずつ扱えるようにしておくと、失敗時の調査が楽になります。
📌 この記事はこんな方におすすめ
@pytest.mark.parametrizeの基本は使えるが、idsやpytest.paramまでは使いこなせていない方- テスト内で
forループを回して、複数のデータを検証している方 - ログインのロールや画面サイズなど、組み合わせテストを整理したいPlaywright(Python)ユーザー
- テストデータをCSVやJSONに外出ししたいが、読み込み方に迷っている方
✅ この記事を読むと得られること
forループとparametrizeの違いと、使い分けの考え方idsで失敗時のテスト名を読みやすくする方法pytest.paramで、ケースごとにxfailやskipを付ける方法indirect=Trueでfixtureにデータを渡す方法
👤 この記事を書いた人
QAエンジニア・テスト自動化エンジニアとして15年以上の実務経験を持つ Yoshi が執筆。データ駆動テストの設計や、pytestとPlaywrightを組み合わせたテスト基盤の整備を実務で担当してきました。
📌 結論
- pytest parametrizeは、1ケースが1テストとして実行・報告されるため、
forループより失敗の切り分けがしやすい idsとpytest.paramでケースに名前と属性を付けると、失敗時の調査やスキップ管理が楽になる- 組み合わせは掛け算で増える。実行時間とメンテナンスの負担を見ながら、必要な範囲に絞る
parametrizeを使い始めると、次のような場面に遭遇しやすくなります。
- 失敗したテストの名前が
test_login[\u30e1\u30fc...]のようになり、どのケースか分からない - 「このケースだけ既知の不具合なので、一時的に外したい」が、全体を止めずにできない
- 乱数を使ったデータで、pytest-xdistの並列実行が収集エラーになる
- 組み合わせを足していくうちに、テスト数が100件を超えて実行が遅くなる
これらは、parametrizeの周辺機能を押さえると整理しやすくなります。
pytest parametrizeとは
@pytest.mark.parametrizeは、1つのテスト関数に複数の入力データを渡し、データの数だけテストを生成する仕組みです。テストのロジックは1つのまま、入力と期待値だけを変えて検証できます。
import pytest
from playwright.sync_api import Page, expect
@pytest.mark.parametrize(
"email, password, message",
[
("", "Passw0rd!", "メールアドレスを入力してください"),
("user@example.com", "", "パスワードを入力してください"),
("invalid", "Passw0rd!", "メールアドレスの形式が正しくありません"),
],
)
def test_login_validation(page: Page, email: str, password: str, message: str) -> None:
page.goto("https://example.com/login") # 実際のURLに置き換える
page.get_by_label("メールアドレス").fill(email)
page.get_by_label("パスワード").fill(password)
page.get_by_role("button", name="ログイン").click()
expect(page.get_by_text(message)).to_be_visible()forループとparametrizeの違い
| 方法 | メリット | デメリット |
|---|---|---|
テスト内のforループ | セットアップが1回で済み、実行が軽い | ループ内でassertが失敗すると、通常はその時点でテスト関数が終了し、残りのケースは検証されない。どの入力で落ちたかも見えにくい |
| parametrize | ケースごとに独立したテスト結果が記録され、失敗した入力を切り分けやすい。個別に実行やスキップができる | ケースごとにfixtureが実行されるため、fixtureのスコープや構成によっては、ページ生成などのコストが増える |
選ぶときの目安:ケースごとに結果を確認したい、特定のケースだけ実行やスキップをしたい場合はparametrizeが向いています。セットアップが重く、1回の流れの中で複数の値をまとめて確認したいだけなら、ループのほうが軽く済みます。
pytest -vで確認できます。実行せずに一覧だけを見たい場合は、pytest --collect-only -qが便利です。この記事のコードはpytest 7.x以降を想定しています。① idsでテスト名を読みやすくする
parametrizeで生成されたテストには、パラメータの値からテストIDが自動生成されます。複数の値を組み合わせる場合やオブジェクトを渡す場合は、意図した名前にならないこともあるため、idsで明示すると管理しやすくなります。日本語などASCII以外の値は、エスケープされた表示になることがあり、読みにくくなる場合があります。
@pytest.mark.parametrize(
"email, password, message",
[
("", "Passw0rd!", "メールアドレスを入力してください"),
("user@example.com", "", "パスワードを入力してください"),
("invalid", "Passw0rd!", "メールアドレスの形式が正しくありません"),
],
ids=["email_empty", "password_empty", "email_invalid_format"],
)
def test_login_validation(page: Page, email: str, password: str, message: str) -> None:
...# ids を付けると、テスト名は次のようになる
tests/test_login.py::test_login_validation[email_empty]
tests/test_login.py::test_login_validation[password_empty]
tests/test_login.py::test_login_validation[email_invalid_format]
# 特定のケースだけを実行できる
pytest "tests/test_login.py::test_login_validation[email_empty]"
pytest -k "email_invalid_format"-kでの絞り込みなど)で、そのまま使えるためです。データクラスでケースをまとめる
from dataclasses import dataclass
@dataclass(frozen=True)
class LoginCase:
case_id: str
email: str
password: str
message: str
CASES = [
LoginCase("email_empty", "", "Passw0rd!", "メールアドレスを入力してください"),
LoginCase("password_empty", "user@example.com", "", "パスワードを入力してください"),
]
@pytest.mark.parametrize("case", CASES, ids=lambda c: c.case_id)
def test_login_validation(page: Page, case: LoginCase) -> None:
page.goto("https://example.com/login") # 実際のURLに置き換える
page.get_by_label("メールアドレス").fill(case.email)
page.get_by_label("パスワード").fill(case.password)
page.get_by_role("button", name="ログイン").click()
expect(page.get_by_text(case.message)).to_be_visible()frozen=Trueにしておくと、属性の再代入による書き換えは防げます。ただし、内部にリストや辞書を持つ場合、その中身の変更までは防げません。② pytest.paramでケースごとに属性を付ける
import pytest
@pytest.mark.parametrize(
"email, password, message",
[
pytest.param("", "Passw0rd!", "メールアドレスを入力してください", id="email_empty"),
pytest.param("user@example.com", "", "パスワードを入力してください", id="password_empty"),
pytest.param(
"a" * 300 + "@example.com",
"Passw0rd!",
"メールアドレスが長すぎます",
id="email_too_long",
marks=pytest.mark.xfail(reason="既知の不具合(チケット管理番号を記載)", strict=True),
),
],
)
def test_login_validation(page: Page, email: str, password: str, message: str) -> None:
...| マーク | 使いどころ |
|---|---|
xfail | 既知の不具合で失敗するケース。修正されて通るようになったら気づける |
skip | 実行しないケース。理由(reason)を必ず書く |
skipif | OSやブラウザなど、条件によって実行を切り替えるケース |
strict=Trueを付けると、想定外にテストが成功した場合(XPASS)を失敗として扱えます。不具合が修正されたのにマークが残り続ける、という見落としを防げます。チームの運用方針に応じて採用を検討してください。skipやxfailを付けたまま放置すると、「見えないテスト」が増えていきます。reasonに不具合の管理番号を書くだけでなく、修正の期限や、マークを外す条件(例:「修正リリース後に外す」)も決めておき、定期的に見直す運用をおすすめします。③ parametrizeを重ねて組み合わせを作る
# 2つ重ねると、全組み合わせ(直積)がテストになる
@pytest.mark.parametrize("width, height", [(375, 667), (1280, 720)], ids=["mobile", "desktop"])
@pytest.mark.parametrize("language", ["ja", "en", "ko"])
def test_top_page(page: Page, width: int, height: int, language: str) -> None:
page.set_viewport_size({"width": width, "height": height})
page.goto(f"https://example.com/{language}/")
...
# → 2 × 3 = 6 件のテストが生成される④ indirect=Trueでfixtureにデータを渡す
通常のparametrizeは、テスト関数の引数に値をそのまま渡します。indirect=Trueを指定すると、同名のfixtureに値が渡され、fixtureの中で加工してからテストに渡せるようになります。ロールごとにログイン状態を作る場面などで便利です。
auth/admin.json と auth/viewer.json を作成し、各ロールでログインした状態を保存しておく必要があります(作り方はstorage_stateの記事を参照)。また、この例ではpagefixtureを使わず、fixture内で独自にコンテキストを作成しています。# conftest.py
from collections.abc import Iterator
import pytest
from playwright.sync_api import Browser, Page
@pytest.fixture
def role_page(request: pytest.FixtureRequest, browser: Browser) -> Iterator[Page]:
role = request.param # parametrize の値がここに渡される
context = browser.new_context(storage_state=f"auth/{role}.json")
page = context.new_page()
yield page
context.close()
# tests/test_admin_menu.py
import pytest
from playwright.sync_api import Page, expect
@pytest.mark.parametrize(
"role_page, expected_visible",
[("admin", True), ("viewer", False)],
indirect=["role_page"],
ids=["admin_can_see", "viewer_cannot_see"],
)
def test_admin_menu(role_page: Page, expected_visible: bool) -> None:
role_page.goto("https://example.com/dashboard")
menu = role_page.get_by_role("link", name="ユーザー管理")
if expected_visible:
expect(menu).to_be_visible()
else:
expect(menu).to_have_count(0)indirect=["role_page"]のように名前を指定すると、そのパラメータだけがfixture経由になり、expected_visibleはそのままテストに渡されます。認証状態ファイルにはCookieなどの認証情報が含まれるため、Git管理から除外し、共有範囲にも注意してください。⑤ JSONファイルからテストデータを読み込む
データファイルは、次のような形式を想定しています。id・email・password・messageのキーが1つでも欠けると、テスト実行前の収集段階でエラーになります。
# tests/data/login_cases.json
[
{
"id": "email_empty",
"email": "",
"password": "Passw0rd!",
"message": "メールアドレスを入力してください"
},
{
"id": "password_empty",
"email": "user@example.com",
"password": "",
"message": "パスワードを入力してください"
}
]import json
from pathlib import Path
from typing import Any
import pytest
from playwright.sync_api import Page, expect
DATA_FILE = Path(__file__).parent / "data" / "login_cases.json"
def load_cases() -> list[Any]:
rows = json.loads(DATA_FILE.read_text(encoding="utf-8"))
return [
pytest.param(row["email"], row["password"], row["message"], id=row["id"])
for row in rows
]
@pytest.mark.parametrize("email, password, message", load_cases())
def test_login_validation(page: Page, email: str, password: str, message: str) -> None:
page.goto("https://example.com/login") # 実際のURLに置き換える
page.get_by_label("メールアドレス").fill(email)
page.get_by_label("パスワード").fill(password)
page.get_by_role("button", name="ログイン").click()
expect(page.get_by_text(message)).to_be_visible()pagefixtureを使うには、pytest-playwrightがインストールされている必要があります。| データの置き場所 | メリット | デメリット |
|---|---|---|
| コード内に直書き | データとテストが同じ場所にあり、追いやすい | 件数が増えるとコードが長くなる |
| JSONなどの外部ファイル | 非エンジニアもデータを編集・追加しやすい | 読み込み時のエラーがテスト収集の段階で出る。型がなく、列の追加漏れに気づきにくい |
.envなどの環境変数から読み込む形にします。実務でよくある失敗パターン
① 日本語や長い値がそのままIDになり、テスト名が読めない
日本語や長い値がIDになると、表示が読みにくくなったり、-kで指定しにくくなったりします。CIのログやコマンドで扱いやすいよう、ASCIIの短いidsを付けておきます。
② 乱数や現在時刻をパラメータに使い、xdistで収集エラーになる
pytest-xdistでは、各ワーカーがテストを収集し、その内容が一致している必要があります。randomやuuid、現在時刻でデータを作ると、ワーカーごとに結果が変わり、収集の不一致エラーになることがあります。値は固定するか、シードを固定します。
③ リストや辞書をパラメータに入れて、テスト間で状態が漏れる
pytestはパラメータ値を自動コピーしません。リストや辞書をテスト内で変更すると、同じオブジェクトを参照する別のケースに影響することがあります。テストデータは変更しない設計にするか、ケースごとに新しいオブジェクトを生成しましょう。なお、frozen=Trueのデータクラスやタプルでも、内部にあるリストや辞書の変更までは防げません。
④ 組み合わせを足し続けて、実行時間が膨らむ
重ねるたびに件数が掛け算で増えます。重要な組み合わせは毎回、残りは夜間のスケジュール実行に分けるなど、実行の頻度を分ける方法もあります。
⑤ 意図の違うケースを1つのparametrizeに詰め込み、テスト内がif分岐だらけになる
ケースごとに検証の中身が違うなら、テスト関数を分けるほうが読みやすくなります。parametrizeは、同じ手順で、データだけが違う場面に向いています。
📖 関連記事
FAQ
Q. fixtureのparams=とparametrizeは、どう違いますか?
fixtureのparams=は、そのfixtureを使うすべてのテストに適用されます。parametrizeは、特定のテスト関数にだけ適用されます。「このデータは、このテストだけで使う」ならparametrize、「多くのテストで同じ組み合わせを使い回す」ならfixtureのparams=が向いています。
Q. 一部のケースだけを実行するには?
idsで付けた名前を使い、pytest -k "email_empty"で絞り込めます。ノードIDをそのまま指定して、pytest "tests/test_login.py::test_login_validation[email_empty]"と実行する方法もあります。
Q. 失敗したケースだけを再実行できますか?
pytest --lf(--last-failed)で、前回失敗したテストだけを再実行できます。parametrizeのケースも、1件ずつ対象になります。
Q. 引数が多くなって読みにくいときは?
データクラスにまとめて、1つの引数(例:case)として渡す方法があります。ids=lambda c: c.case_idのようにIDも付けやすくなります。
Q. Seleniumのテストでも同じように使えますか?
はい。parametrizeはpytestの機能なので、SeleniumでもPlaywrightでも使えます。違いは、ケースごとのブラウザの準備にかかるコストです。ブラウザやWebDriverの起動・初期化がケースごとに必要な構成では、実行時間が増えやすいため、組み合わせの数に注意します。
📋 まとめ
- pytest parametrizeは、1ケースが1テストになり、
forループより失敗の切り分けがしやすい idsはASCIIの短い名前にして、失敗時のログと-kでの絞り込みに使えるようにするpytest.paramのmarksで、ケース単位にxfail・skipを付ける。理由は必ず書くindirectを使うと、fixtureを通してデータを加工できる- 組み合わせは掛け算で増えるため、必要な範囲に絞る
- 乱数・可変オブジェクトは、xdistや状態漏れの原因になりやすい
まずは、いまテスト内でforループを回している箇所を1つ選んで、ids付きのparametrizeに置き換えてみてください。失敗したときに、どのケースか一目で分かるようになります。
