テストを書き始めると、よくこんな状況に遭遇します。
- テストを書いたら不具合が大量に見つかった
- 不具合は認識しているが今すぐは直せない
その結果、大量の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 を単なる「失敗してもよいテスト」ではなく「既知不具合を管理するためのマーカー」として使うのが非常に有効だと考えています。
運用ルールをまとめると次のとおりです。
- 仕様どおりの期待値でテストを書く
- FAILしたら不具合を起票する
- 後回しにするものへ xfail(strict=True) を付ける
- mainブランチには PASS と XFAIL だけを残す
- 不具合修正時は xfail を削除する
- 最終的に XFAIL をゼロへ近付ける
こうすることで、テストコードが単なる品質確認ツールではなく、「既知不具合の台帳」としても機能するようになるのでは、と考えています。

