Pythonテスト #3 parametrizeとマーカー: ケースを増やして選んで回す

#1 pytest 入門 で最初のテストを回し、#2 フィクスチャ で準備コードを整理しました。今回はテスト本文の繰り返しを減らす番です。同じ関数を入力だけ変えながら検証するために、ほとんど同じテストをコピーした経験があるなら、@pytest.mark.parametrize がその繰り返しをなくしてくれます。後半では マーカー でテストにラベルを付けて、必要なものだけ選んで回す方法まで扱います。

同じテストを 3 回コピーしているなら #

🚫 入力だけが違うテスト 3 つ
def test_add_positive():
    assert add(1, 2) == 3

def test_add_negative():
    assert add(-1, -2) == -3

def test_add_zero():
    assert add(0, 0) == 0

ロジックは全部 assert add(a, b) == expected の 1 行です。違うのは数字だけなのに、関数が 3 つあります。ケースを追加するたびに関数名を新しく考えて本文をコピーすることになり、検証方法が変わると 3 ヶ所を一緒に直すことになります。

@pytest.mark.parametrize: ケース表がそのまま仕様 #

✅ ケースを表に
import pytest
from calculator import add

@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (-1, -2, -3),
    (0, 0, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

最初の引数はパラメータ名で、2 番目の引数はケースのリストです。pytest がケースごとにテストを 1 回ずつ回すので、関数は 1 つでも pytest -v で見ると test_add[1-2-3]test_add[-1--2--3]test_add[0-0-0] の 3 件として集計されます。ケースのリスト自体が「この関数はこの入力でこの結果を返す」という仕様の役割を果たし、新しいケースの追加はタプル 1 行の追加で終わります。

[-1--2--3] のような自動 ID は読みにくいです。ケースリストの後ろに ids=["positive", "negative", "zero"] を足すと、結果が test_add[negative] のように表示され、失敗したときにどのケースが壊れたのかがすぐ見えます。ケースが増えるほど効果が大きいです。

parametrize の重ねがけ: 組み合わせの自動生成 #

デコレーターを 2 回重ねると、すべての組み合わせが作られます。

組み合わせの生成
@pytest.mark.parametrize("role", ["admin", "member"])
@pytest.mark.parametrize("active", [True, False])
def test_permission(role, active):
    assert can_login(role, active) == active

2×2 で 4 個のケースが自動的に生まれます。ただしパラメータが増えるとケース数が掛け算で増えるので、本当にすべての組み合わせを検証すべきなのかを考えてから使うのが良いです。

エラーケースも表に: pytest.raises との組み合わせ #

境界値と不正な入力こそ、parametrize が最も輝く領域です。

エラーケース集
@pytest.mark.parametrize("bad_input", ["", "abc", "-1", "200"])
def test_parse_age_rejects(bad_input):
    with pytest.raises(ValueError):
        parse_age(bad_input)

正常ケースの表とエラーケースの表を並べておけば、関数が何を受け取り何を拒否するのかが、テストファイルを見るだけでわかります。

skip、skipif、xfail: 3 つのマーカーの意味の違い #

マーカーはテストに付けるラベルです。まず pytest に組み込まれている 3 つを区別します。

組み込みマーカー 3 つ
@pytest.mark.skip(reason="決済モジュールの差し替え作業中")
def test_legacy_payment():
    ...

@pytest.mark.skipif(sys.platform == "win32", reason="Unix 専用機能")
def test_unix_socket():
    ...

@pytest.mark.xfail(reason="issue #142: マイナス金額のバグ", strict=True)
def test_negative_amount():
    assert charge(-1000) is None
マーカー意味
skip今は回しません。理由を reason に残します
skipif条件が真のときだけスキップします (OS、Python バージョンなど)
xfail失敗することがわかっています。既知のバグの文書化です

特に便利なのが xfail です。バグを見つけたもののすぐには直せないとき、テストを消す代わりに xfail として残しておけば、バグが記録として残ります。strict=True を付けておくと、バグが直ってテストが通った瞬間に XPASS 失敗として知らせてくれるので、マーカーを外すタイミングも逃しません。

カスタムマーカー: 登録して -m で選んで回す #

マーカーは自分で定義することもできます。まず pyproject.toml に登録します。登録していないマーカーは警告が出て、--strict-markers オプションを付けるとエラーになります。

pyproject.toml
[tool.pytest.ini_options]
markers = [
    "slow: 時間のかかるテスト",
    "integration: 外部サービスが必要なテスト",
]
マーカーの付与
@pytest.mark.slow
def test_full_report():
    ...

実行するときは -m-k オプションで選びます。

選んで実行
pytest -m slow                        # slow マーカーだけ
pytest -m "not slow"                  # slow を除外
pytest -m "integration and not slow"  # and、or、not を組み合わせ可能
pytest -k parse                       # 名前に parse を含むテストだけ

-m はマーカーで、-kテスト名 で選びます。-k はマーカーを付けていなくても動作し、pytest -k "parse and not rejects" のような組み合わせもできます。

slow マーカーの運用パターン #

カスタムマーカーの最も一般的な使い道は、遅いテストの分離です。スイート全体が遅くなると開発中にテストを回さなくなり、その瞬間からテストの価値が下がります。

  • 1 秒以上かかるテストに @pytest.mark.slow を付けます
  • 開発中は pytest -m "not slow" で速く回します
  • コミット前や CI では pytest で全体を回します

この区別だけで「頻繁に回る速いスイート」と「ときどき回る全体スイート」という 2 つの層ができます。CI 側の運用は第 7 回で続けて扱います。

まとめ #

  • @pytest.mark.parametrize: 入力だけが違うテストをケース表 1 つにまとめます
  • ids: ケースに読みやすい名前を付けます
  • デコレーターの重ねがけ: パラメータの組み合わせを自動生成します
  • pytest.raises との組み合わせ: エラーケースと境界値も表で仕様化します
  • skip/skipif/xfail: スキップ、条件付きスキップ、既知のバグの文書化です
  • カスタムマーカー + -m、名前ベースの -k: 必要なテストだけ選んで実行します
  • slow マーカー: 速いスイートと全体スイートを分けて運用します

次回 (#4 mock と monkeypatch) では、外部に依存するコードをテストするツールを扱います。時間、環境変数、ネットワーク呼び出しのように、テストが制御できないものを偽物に差し替える方法です。

X