pytestの xfail を既知不具合管理に使いたい

四角ズボン
2026-09-18
2026-09-18

テストを書き始めると、よくこんな状況に遭遇します。

  • テストを書いたら不具合が大量に見つかった
  • 不具合は認識しているが今すぐは直せない

その結果、大量のFAILが残り続けて既知不具合と新規不具合を区別しづらくなります。
「FAILしているのが当たり前」という状態になり、せっかくのテストが品質ゲートとして機能しなくなってしまいます。

私は最近、こうした問題を管理するために pytest の xfail を活用する運用を考えました。

この記事では、xfail を「既知不具合管理ツール」として使う方法を紹介します。

なお本記事では、@pytest.mark.xfail のようなマーカーを xfail、テスト実行時の結果表示を xfailed、そして「既知不具合として管理されている状態」やその運用を指す総称として XFAIL を使い分けています。

xfailとは

xfail は expected failure(失敗することが期待されているテスト) を表す pytest の機能です。

import pytest

@pytest.mark.xfail
def test_sample():
    assert 1 == 2

このテストは失敗しますが、「この失敗は想定済み」として扱われます。

結果は次のようになります。

  • 1 xfailed

通常の FAIL とは区別して報告されます。

よくある問題

例えば新しいテスト基盤を作るとします。まずは仕様どおりの期待値でテストを作成します。

def test_total_price():
   
assert calc_price(100, 0.1) == 110

テストを全部実行すると、

  • 100 passed
  • 15 failed

になったとします。ここでありがちなのが、

  • テストをコメントアウトする
  • 一時的に期待値を変える
  • 無視してFAILを放置する

という対応です。しかしこれでは、「どのFAILが既知不具合なのか」 「新しい不具合が発生したのか」が分からなくなります。

私が考えている運用フロー

1. 設計どおりの期待値で全テストを作成する

実装ではなく設計を基準にします。

def test_total_price():
    assert calc_price(100, 0.1) == 110

2. 一度すべて実行する

pytest

FAILしたからといって期待値は変えません。不具合を洗い出します。

3. 実装不具合によるFAILを1件ずつ起票する

例えば

  • BUG-101 消費税計算の誤り
  • BUG-102 ゼロ除算時の例外処理不備

のようにチケットを作成します。
ここで重要なのは FAILを放置せず、必ず管理対象にする ことです。

4. 修正を後回しにするテストへ xfail(strict=True) を付ける

import pytest

@pytest.mark.xfail(
     strict=True,
    reason="BUG-101: 消費税計算が誤っている"
)
def test_total_price():
    assert calc_price(100, 0.1) == 110

これでテストは実行されますが、失敗しても既知不具合として扱われます。

5. PASS + XFAIL の状態でマージする

理想状態は次のようになります。

  • 100 passed
  • 15 xfailed
  • 0 failed
  • 0 error

ここで重要なのは

  • 0 failed
  • 0 error

であることです。FAILが残っている状態は許可しません。

6. 各不具合チケットで実装を修正する

BUG-101 の対応を始めます。
実装を修正します。

def calc_price(price, tax):
    # return int(price * tax) # 修正前
    return int(price * (1 + tax)) # 修正後

7. 同じPRで xfail マーカーを削除する

修正が終わったら、

@pytest.mark.xfail(
    strict=True,
    reason="BUG-101: 消費税計算が誤っている"
)

を削除します。

def test_total_price():
    assert calc_price(100, 0.1) == 110

通常のテストに戻します。

8. PASSになることを確認してマージする

結果は

  • 101 passed
  • 14 xfailed

となります。1件ずつ既知不具合が減っていきます。

なぜ strict=True を使うのか

私がこの運用で最も重要だと思っているのが strict=True です。

@pytest.mark.xfail(strict=True)

を付けると、本来失敗するはずのテストが成功した場合に失敗(FAILED)になります。

例えば

@pytest.mark.xfail(strict=True)
def test_bug():
    assert True

実行結果:

  • FAILED test_bug - [XPASS(strict)]

これは pytest 全体として失敗扱いになります。
つまり、バグは直ったのに xfail を消し忘れた という状態を検出できます。

xfail と skip の違い

よく比較されるのが skip です。

skip

@pytest.mark.skip(reason="未実装")
def test_sample():
     ...

テストを実行しません。

xfail

@pytest.mark.xfail(
    strict=True,
    reason="BUG-101"
)
def test_sample():
     ...

テストを実行します。

結果を観測し続けながら、 既知不具合として管理できます。不具合管理用途では skip より xfail の方が向いています

CIを導入したらさらに強い

現時点では CI は導入できていないのですが、将来的には非常に相性が良いと思っています。
理想的には:

  • PASS → OK
  • XFAIL → OK
  • FAIL → NG
  • XPASS(strict) → NG

となります。すると、

  • 新しい不具合が発生した
  • xfail を消し忘れた
  • 想定外の挙動が起きた

といった問題を自動で検知できるようになる想定です。

まとめ

私は xfail を単なる「失敗してもよいテスト」ではなく「既知不具合を管理するためのマーカー」として使うのが非常に有効だと考えています。

運用ルールをまとめると次のとおりです。

  1. 仕様どおりの期待値でテストを書く
  2. FAILしたら不具合を起票する
  3. 後回しにするものへ xfail(strict=True) を付ける
  4. mainブランチには PASS と XFAIL だけを残す
  5. 不具合修正時は xfail を削除する
  6. 最終的に XFAIL をゼロへ近付ける

こうすることで、テストコードが単なる品質確認ツールではなく、「既知不具合の台帳」としても機能するようになるのでは、と考えています。