メインコンテンツへスキップ

pytestのデコレータ実践ガイド(parametrize / fixture / mark / skip / xfail)

🧭 はじめに

このページでは、pytestで頻出する「デコレータ(decorator)」を、実務で迷わず使える粒度で整理します。

特に、

  • テストケースが増えて重複がつらい
  • 「条件付きでskipしたい」
  • 「失敗しても良いテストを管理したい」
  • 「テストをカテゴリ分けしたい」
  • fixtureをどのスコープで使うべきか覚えてない

みたいな場面で、忘れても復帰できることを狙います。

結論:pytestのデコレータは「冗長なテストを短くする」ための道具で、正しく使うとテストの可読性と保守性が上がります。


🧩 pytestの「デコレータ」とは何か?

pytestでのデコレータは、ざっくり以下の用途に分かれます。

  • テストを 増やす(parameterize)
  • テストを 分類する(mark)
  • テストを 止める / 条件で止める(skip / xfail)
  • fixture(前処理)に 設定を与える(fixture decorator)

つまり「テスト関数の見た目を保ちつつ、振る舞いを追加する」手段です。


🧪 @pytest.mark.parametrize(最重要)

✅ 何のために使う?

同じテスト構造で入力だけ変えたいときの王道です。

  • if分岐でまとめるのはNGになりがち
  • テスト関数をコピーしまくるのもNG

その間を埋める最強の仕組みが parametrize


✅ 基本形(1引数)

import pytest

@pytest.mark.parametrize('x', [1, 2, 3])
def test_is_positive(x: int) -> None:
    assert x > 0

この1個の関数が 3つのテストとして実行されます。


✅ 2引数以上(典型)

import pytest

@pytest.mark.parametrize(
    ('a', 'b', 'expected'),
    [
        (1, 2, 3),
        (10, 20, 30),
    ],
)
def test_add(a: int, b: int, expected: int) -> None:
    assert a + b == expected

おすすめ:パラメータは(a,b,expected)形式が読みやすく、失敗時も追いやすい。


✅ 異常系(例外)テストを短くする

import pytest

@pytest.mark.parametrize(
    ('bad_book_id',),
    [
        (0,),
        (-1,),
        (None,),
        ('1',),
    ],
)
def test_validate_bad_book_id(bad_book_id: object) -> None:
    with pytest.raises(ValueError):
        # validate_and_normalize(...)
        ...

こういう「ダメ入力集合」は parametrize に押し込むのが鉄板です。


id=(失敗時に読みやすくする)

parametrizeはデフォルトだと (0,) みたいな表示で分かりにくいことがあります。 idを付けるとログが読みやすくなります。

import pytest

@pytest.mark.parametrize(
    'bad_book_id',
    [0, -1, None, '1'],
    ids=['zero', 'negative', 'none', 'string'],
)
def test_validate_bad_book_id(bad_book_id: object) -> None:
    ...

idsは「デバッグのため」に付ける。必須ではないが、時間が経つほど効く。


pytest.param()(個別にidやマークを付ける)

import pytest

@pytest.mark.parametrize(
    'x',
    [
        pytest.param(1, id='small'),
        pytest.param(1000, id='large'),
    ],
)
def test_something(x: int) -> None:
    ...

さらに「このケースだけskip」みたいな運用も可能。

import pytest

@pytest.mark.parametrize(
    'x',
    [
        pytest.param(1, id='ok'),
        pytest.param(999, marks=pytest.mark.skip(reason='まだ仕様未確定'), id='pending'),
    ],
)
def test_case(x: int) -> None:
    ...

🧰 @pytest.fixture(前処理の王道)

✅ 何のために使う?

テストの前後処理を「見える化」しつつ使い回すためです。

  • 毎回同じ準備を書くのはダルい
  • でも setup() みたいな暗黙の仕組みは追いづらい

pytestは fixture を明示的に差し込む設計なので読みやすいです。


✅ 基本形

import pytest

@pytest.fixture
def base_config() -> dict:
    return {'book_id': 1, 'chapters': [{'name': '01', 'pages': []}]}


def test_validate_ok(base_config: dict) -> None:
    ...
  • base_config 引数に書くだけで注入される
  • 引数名がfixture名

scope= の意味(重要)

fixtureは「どの単位で使い回されるか」を選べます。

  • function(デフォルト):テスト関数ごとに生成
  • class:クラスごと
  • module:ファイルごと
  • session:pytest全体で1回
import pytest

@pytest.fixture(scope='session')
def expensive_resource():
    ...

sessionスコープは便利だが注意:テスト間で状態を共有して壊れやすい。基本は function が安全。


✅ fixtureにパラメータを渡したい場合(fixtureのparam化)

import pytest

@pytest.fixture(params=[1, 2, 3])
def n(request) -> int:
    return request.param

ただし、読みにくくなりやすいので、単純な場合は parametrize を優先するのが無難。


🏷️ @pytest.mark.*(分類・制御)

✅ markの用途

  • テストをカテゴリ分けする
  • 実行対象を絞る
  • CIやローカルで切り替える

典型:

  • slow(遅いテスト)
  • integration(統合テスト)
  • network(外部通信あり)

✅ 例:@pytest.mark.slow

import pytest

@pytest.mark.slow
def test_big_data() -> None:
    ...

実行を絞る:

pytest -m slow
pytest -m "not slow"

ローカルは全部回さない運用にしたいなら、markが最強のスイッチになる。


✅ markを新規に作るときの注意(pytest.ini推奨)

markは自由に作れますが、登録しないと警告が出ることがあります。

pytest.ini

[pytest]
markers =
    slow: time-consuming tests
    integration: integration tests
    network: tests that access network

markを運用するならpytest.iniは早めに用意しておくと安定する。


⏭️ @pytest.mark.skip(無条件スキップ)

✅ 使いどころ

「今は仕様未確定」「一時的に無効化したい」など。

import pytest

@pytest.mark.skip(reason='仕様確定待ち')
def test_pending_feature() -> None:
    ...

🧠 @pytest.mark.skipif(条件付きスキップ)

✅ 典型例:OSやPythonバージョンで分岐

import sys
import pytest

@pytest.mark.skipif(sys.platform.startswith('win'), reason='Windowsでは再現しない')
def test_linux_only() -> None:
    ...

Pythonバージョン条件:

import sys
import pytest

@pytest.mark.skipif(sys.version_info < (3, 13), reason='Python 3.13+ 前提')
def test_requires_313() -> None:
    ...

skipifは「環境差異の吸収」に使う。仕様未確定の逃げ道として多用すると腐りやすい。


🧯 @pytest.mark.xfail(失敗しても良いテスト)

✅ 何のために使う?

「失敗することが分かっているが、残しておきたい」テストを管理するため。

  • 既知バグの再現テスト
  • 将来直す予定
  • ただしCIを赤くしたくない
import pytest

@pytest.mark.xfail(reason='既知バグ: issue #123')
def test_known_bug() -> None:
    assert do_something() == 1

strict=True(直ったら失敗扱い)

xfail は「失敗するはず」と言ってるので、直って通った場合に気づきたいことがあります。 その場合は strict。

import pytest

@pytest.mark.xfail(strict=True, reason='既知バグ: fixされたらxfailを消す')
def test_known_bug() -> None:
    ...

strict=True は有能:直ったのにxfailが残ってる状態を検知できる。


🧱 @pytest.mark.usefixtures(引数に書かずfixture適用)

✅ 使いどころ

fixtureは普通「引数に書く」のが分かりやすいですが、 テスト関数の引数を増やしたくない場合に使えます。

import pytest

@pytest.mark.usefixtures('setup_db')
def test_something() -> None:
    ...

ただし、これはやりすぎると「何が注入されてるか見えない」ので乱用しない。

usefixtures多用は読みづらくなる。基本は引数で明示が正義。


🧬 デコレータを組み合わせるときの型

✅ parametrize + mark

import pytest

@pytest.mark.slow
@pytest.mark.parametrize('x', [1, 2, 3])
def test_heavy(x: int) -> None:
    ...

または、paramごとにmark:

import pytest

@pytest.mark.parametrize(
    'x',
    [
        pytest.param(1, id='fast'),
        pytest.param(2, marks=pytest.mark.slow, id='slow_case'),
    ],
)
def test_mixed(x: int) -> None:
    ...

🧭 実務でのおすすめ優先順位

迷ったらこの優先順位が安全。

  1. @pytest.mark.parametrize(重複削減)
  2. @pytest.fixture(前処理共通化)
  3. @pytest.mark.skipif(環境差異吸収)
  4. @pytest.mark.slow 等(分類)
  5. @pytest.mark.xfail(strict=True)(既知バグ管理)

よくある事故:xfail/skipを増やしすぎて「テストが通っているのか分からない」状態になる。最小限にする。


✅ まとめ

  • pytestのデコレータは「テストの重複を減らし、管理しやすくする」道具
  • parametrize は最重要(異常系入力集合もここに押し込む)
  • fixture は前処理の見える化・共通化の基本
  • mark は分類・選別に使う(pytest -m と相性が良い)
  • skipif は環境差異の吸収
  • xfail(strict=True) は既知バグの管理に強い

このセットを覚えておけば、pytestの運用が途切れても復帰しやすい。