閉じる

データ駆動テスト入門: pytestのparametrizeで複数パターンを一括検証

テストを書きやすく、かつ保守しやすくするには、同じ処理を異なる入力で繰り返し検証することが重要です。

pytestのparametrizeを使うと、1つのテスト関数に複数のデータセットを与えてデータ駆動テストを簡単に実現できます。

ここではPython初心者の方でもすぐに使える形で、基本から読みやすさの工夫、つまずきやすいポイントまで丁寧に解説します。

pytestのparametrizeとは

@pytest.mark.parametrizeは、テスト関数に与える引数と値の組(データセット)を列挙して、同じテストを複数パターンで自動実行するためのデコレータです。

テストコードの重複を減らし、可読性とメンテナンス性を高めます。

1つのテストで複数データを一括検証するメリット

データを横に並べて管理できるため、テスト関数を量産せずに網羅性を高められます。

失敗時はどのデータで壊れたかが明確に分かるため、原因追跡が早くなります。

さらにテストケースの追加・削除が配列の編集だけで完了し、レビューもしやすくなります。

重複を減らしテストを読みやすくするポイント

テストの本質は何を入れて何を期待するかです。

parametrizeでは、データを一箇所に集約し、テスト本文は1つの振る舞いに集中させましょう。

ケース名をidsで明示したり、pytest.paramで個別にskip/xfailを付けると、さらに見通しがよくなります。

pytest parametrizeの基本的な使い方

単一引数の例(値のチェック)

偶数判定関数is_evenに対して、偶数だけをまとめて検証します。

テスト引数はnの1つです。

Pythoncalc.py
# file: calc.py
def is_even(n: int) -> bool:
    """偶数ならTrueを返します。"""
    return n % 2 == 0
Pythontest_even_parametrize.py
# file: tests/test_even_parametrize.py
import pytest
from calc import is_even

# nに複数の値を渡し、同じアサーションを繰り返す
@pytest.mark.parametrize("n", [0, 2, 10, 42])  # 必ず偶数だけを並べています
def test_is_even_returns_true_for_even_numbers(n):
    # 単一引数の例では、1つの性質(偶数→True)だけに集中します
    assert is_even(n) is True

実行例(静かめの出力にするため-qを付けます)。

実行結果
$ pytest -q
....                                                                 [100%]
4 passed in 0.03s

複数引数の例(入力と期待値)

足し算関数addの入力2つと期待値1つの計3引数をまとめてパラメータ化します。

Python
# file: calc.py
def add(a: float, b: float) -> float:
    """2数の和を返します。"""
    return a + b


# file: tests/test_add_parametrize.py
import pytest
from calc import add

# "a,b,expected"の順で引数名を定義し、各データをタプルで与えます
@pytest.mark.parametrize(
    "a,b,expected",
    [
        (1, 2, 3),
        (-1, 5, 4),
        (0, 0, 0),
        (2.5, 0.5, 3.0),
    ],
)
def test_add_returns_expected(a, b, expected):
    # Arrange(準備)はparametrizeで済み、Act(実行)とAssert(検証)に集中できます
    assert add(a, b) == expected
実行結果
$ pytest -q
....                                                                 [100%]
4 passed in 0.03s

以下はparametrizeの主な引数まとめです。

引数名役割
argnamesstr または list[str]テスト関数の引数名を指定"a,b,expected" または ["a", "b", "expected"]
argvalueslist各実行で渡す値の集合[(1,2,3), (0,0,0)]
idslist[str] または callable各ケースに分かりやすい名前を付けるids=["small", "zero"]
markspytest.mark(個別指定)個々のケースにマークを付けるpytest.param(..., marks=pytest.mark.xfail(...))

サンプルの実行方法(pytest -q)

  • 事前にpip install pytestでインストールします。
  • プロジェクト直下でpytest -qを実行するとテストが走ります。
  • 特定のテストだけ実行したい場合はpytest -q -k "add"のようにキーワード指定が便利です。
  • 詳細なテスト名を表示したい場合は-vを付けます。

コマンドは最低限pytest -qだけ覚えておけば十分です。

失敗例やスキップ・期待失敗の状態はサマリに分かりやすく集計されます。

読みやすくする工夫(idsとpytest.param)

idsでテスト名をわかりやすくする

idsを使うと、各データセットに人が読める名前を付けられます。

失敗時のログが「どのパターンで壊れたか」一目瞭然になります。

Python
# file: tests/test_add_with_ids.py
import pytest
from calc import add

@pytest.mark.parametrize(
    "a,b,expected",
    [
        (1, 2, 3),
        (-1, 5, 4),
        (0, 0, 0),
    ],
    ids=["small-positives", "with-negative", "all-zero"],
)
def test_add_readable_ids(a, b, expected):
    assert add(a, b) == expected

詳細表示で実行するとIDが括弧内に出ます:

実行結果
$ pytest -q -k test_add_readable_ids -vv
tests/test_add_with_ids.py::test_add_readable_ids[small-positives] PASSED
tests/test_add_with_ids.py::test_add_readable_ids[with-negative] PASSED
tests/test_add_with_ids.py::test_add_readable_ids[all-zero] PASSED

pytest.paramで個別にskipやxfailを付ける

pytest.paramは、各データにmarksidを付けたい時に使います。

未対応のケースにxfail、環境依存で動かせないケースにskipなどを指定できます。

Python
# file: tests/test_divide_param_marks.py
import pytest

def divide(a: float, b: float) -> float:
    """単純な割り算(ゼロ除算は現状未対応の想定)。"""
    return a / b

@pytest.mark.parametrize(
    "a,b,expected",
    [
        (6, 3, 2),
        (5, 2, 2.5),
        # 未対応の0除算は、意図的にxfailで印を付けておきます
        pytest.param(1, 0, None, marks=pytest.mark.xfail(reason="未対応: 0除算"), id="zero-division"),
        # デモ目的で一時的にスキップ
        pytest.param(10, 5, 2, marks=pytest.mark.skip(reason="デモ: スキップ"), id="skip-demo"),
    ],
)
def test_divide(a, b, expected):
    # 0除算ケースは例外で失敗しますが、xfail指定により「期待された失敗」として扱われます
    assert divide(a, b) == expected
実行結果
$ pytest -q -k test_divide
..xs                                                                 [100%]
2 passed, 1 skipped, 1 xfailed in 0.03s
補足

xfail(strict=True)とすると、将来そのケースが偶然通るようになった時にXPASSを失敗として検知できます。

仕様が直ったのにマーク消し忘れ…を防げます。

エッジケース(0や空文字)をまとめて検証

境界値や空データはバグが潜みがちです。

代表的なエッジケースをデータ化して一気に押さえましょう。

Python
# file: text_utils.py
def is_blank(s: str) -> bool:
    """空文字または空白のみをTrueにします。"""
    return s.strip() == ""


# file: tests/test_edge_cases.py
import pytest
from calc import is_even
from text_utils import is_blank

# 数値のエッジケース: 0は偶数
@pytest.mark.parametrize("n", [0, 2, 100])
def test_is_even_including_zero(n):
    assert is_even(n) is True

# 文字列のエッジケース: 空文字と空白のみ
@pytest.mark.parametrize(
    "s,expected",
    [
        ("", True),
        ("   ", True),
        ("abc", False),
    ],
    ids=["empty", "spaces-only", "normal"],
)
def test_is_blank(s, expected):
    assert is_blank(s) is expected
実行結果
$ pytest -q -k "test_is_even_including_zero or test_is_blank"
.....                                                                [100%]
5 passed in 0.03s

つまずきポイントと対策

引数名とデータの並びを一致させる

引数名の順と、値のタプルの並びは必ず一致させます。

例えば"a,b,expected"と宣言したら、値は(a, b, expected)の順で並べます。

Python
# 悪い例: 並びが不一致
@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    # 本来は (a, b, expected) だが (expected, a, b) になっている
    (3, 1, 2),
])
def test_add_mismatch(a, b, expected):
    ...

このような不一致はテストの意図せぬ失敗を招き、原因特定が難しくなります。

最初に引数名を決め、各タプルの並びを統一しましょう。

データ数と引数数の不一致エラーを防ぐ

引数名の個数と、各データタプルの要素数が一致しないと、pytestが直ちにエラーにしてくれます。

エラーメッセージは次のようになります。

Python
# file: tests/test_bad_arity.py
import pytest

@pytest.mark.parametrize("a,b,expected", [
    (1, 2),  # expectedが欠けている
])
def test_arity(a, b, expected):
    ...
実行結果
$ pytest -q
E   ValueError: wrong number of values: expected 3, got 2

対策はシンプルで、タイポやコピペ時の列ズレをレビューで重点確認することです。

ケースが多くなるなら、idsを付けておくとズレにすぐ気付けます。

文字列や辞書など非数値データの渡し方

parametrizeは数値専用ではありません。

文字列や辞書、タプル、Noneなど任意のPythonオブジェクトを渡せます。

  • 単一の引数として辞書を渡す例:
Python
# file: tests/test_dict_param.py
import pytest

@pytest.mark.parametrize(
    "user",
    [
        {"name": "Alice", "age": 20},
        {"name": "Bob", "age": 0},
    ],
    ids=["alice", "bob"],
)
def test_user_age_nonnegative(user):
    assert user["age"] >= 0
  • 複数引数に非数値を渡す場合は、(arg1, arg2, ...)のタプルで並べます:
Python
# file: tests/test_strings_param.py
import pytest

def greet(name: str, suffix: str) -> str:
    return f"Hello, {name}{suffix}"

@pytest.mark.parametrize(
    "name,suffix,expected",
    [
        ("World", "!", "Hello, World!"),
        ("Python", " 3.12", "Hello, Python 3.12"),
    ],
)
def test_greet(name, suffix, expected):
    assert greet(name, suffix) == expected
注意

複数引数に辞書のキーを展開して割り当てたい場合でも、{"a": 1, "b": 2}のような辞書をそのまま(a, b)に自動展開はしません。

必ずタプルの位置引数で渡すか、引数を1つdataにまとめて辞書を受け取り、その中身をテスト内で参照するようにしましょう。

まとめ

pytestのparametrizeは、同じテストを複数データで一括実行できる強力な仕組みです。

データを並べるだけで網羅性を上げられ、テスト本文は振る舞いに集中できます。

さらにidsで可読性を高め、pytest.paramskip/xfailを適切に使い分ければ、長期運用でも迷いにくいテストになります。

エッジケース(0や空文字)を積極的にデータ化し、引数名と値の並びや要素数の不一致といった初歩的なミスを避けることで、テストはより堅牢になります。

まずは小さな関数から単一引数複数引数idsやmarksの順に慣れていき、日々の実装にデータ駆動テストを自然に取り込んでいきましょう。

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

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

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

URLをコピーしました!