テストを書きやすく、かつ保守しやすくするには、同じ処理を異なる入力で繰り返し検証することが重要です。
pytestのparametrize
を使うと、1つのテスト関数に複数のデータセットを与えてデータ駆動テストを簡単に実現できます。
ここではPython初心者の方でもすぐに使える形で、基本から読みやすさの工夫、つまずきやすいポイントまで丁寧に解説します。
pytestのparametrizeとは
@pytest.mark.parametrize
は、テスト関数に与える引数と値の組(データセット)を列挙して、同じテストを複数パターンで自動実行するためのデコレータです。
テストコードの重複を減らし、可読性とメンテナンス性を高めます。
1つのテストで複数データを一括検証するメリット
データを横に並べて管理できるため、テスト関数を量産せずに網羅性を高められます。
失敗時はどのデータで壊れたかが明確に分かるため、原因追跡が早くなります。
さらにテストケースの追加・削除が配列の編集だけで完了し、レビューもしやすくなります。
重複を減らしテストを読みやすくするポイント
テストの本質は何を入れて何を期待するかです。
parametrize
では、データを一箇所に集約し、テスト本文は1つの振る舞いに集中させましょう。
ケース名をids
で明示したり、pytest.param
で個別にskip
/xfail
を付けると、さらに見通しがよくなります。
pytest parametrizeの基本的な使い方
単一引数の例(値のチェック)
偶数判定関数is_even
に対して、偶数だけをまとめて検証します。
テスト引数はn
の1つです。
# file: calc.py
def is_even(n: int) -> bool:
"""偶数ならTrueを返します。"""
return n % 2 == 0
# 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引数をまとめてパラメータ化します。
# 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
の主な引数まとめです。
引数名 | 型 | 役割 | 例 |
---|---|---|---|
argnames | str または list[str] | テスト関数の引数名を指定 | "a,b,expected" または ["a", "b", "expected"] |
argvalues | list | 各実行で渡す値の集合 | [(1,2,3), (0,0,0)] |
ids | list[str] または callable | 各ケースに分かりやすい名前を付ける | ids=["small", "zero"] |
marks | pytest.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
を使うと、各データセットに人が読める名前を付けられます。
失敗時のログが「どのパターンで壊れたか」一目瞭然になります。
# 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
は、各データにmarks
やid
を付けたい時に使います。
未対応のケースにxfail
、環境依存で動かせないケースにskip
などを指定できます。
# 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や空文字)をまとめて検証
境界値や空データはバグが潜みがちです。
代表的なエッジケースをデータ化して一気に押さえましょう。
# 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)
の順で並べます。
# 悪い例: 並びが不一致
@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が直ちにエラーにしてくれます。
エラーメッセージは次のようになります。
# 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オブジェクトを渡せます。
- 単一の引数として辞書を渡す例:
# 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, ...)
のタプルで並べます:
# 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.param
でskip
/xfail
を適切に使い分ければ、長期運用でも迷いにくいテストになります。
エッジケース(0や空文字)を積極的にデータ化し、引数名と値の並びや要素数の不一致といった初歩的なミスを避けることで、テストはより堅牢になります。
まずは小さな関数から単一引数→複数引数→idsやmarksの順に慣れていき、日々の実装にデータ駆動テストを自然に取り込んでいきましょう。