警告をキャプチャする方法

バージョン 3.1 から、pytest はテスト実行中に警告を自動的にキャッチし、セッションの最後に表示します:

# content of test_show_warnings.py
import warnings


def api_v1():
    warnings.warn(UserWarning("api v1, should use functions from v2"))
    return 1


def test_one():
    assert api_v1() == 1

pytest を実行すると、次の出力が生成されます:

$ pytest test_show_warnings.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item

test_show_warnings.py .                                              [100%]

============================= warnings summary =============================
test_show_warnings.py::test_one
  /home/sweet/project/test_show_warnings.py:5: UserWarning: api v1, should use functions from v2
    warnings.warn(UserWarning("api v1, should use functions from v2"))

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================= 1 passed, 1 warning in 0.12s =======================

警告の制御

Python の warning filter および -W option フラグと同様に、pytest は独自の -W フラグを提供して、無視される警告、表示される警告、エラーに変換される警告を制御します。 より高度な使用例については、warning filter ドキュメントを参照してください。

このコードサンプルは、任意の UserWarning カテゴリの警告クラスをエラーとして扱う方法を示しています:

$ pytest -q test_show_warnings.py -W error::UserWarning
F                                                                    [100%]
================================= FAILURES =================================
_________________________________ test_one _________________________________

    def test_one():
>       assert api_v1() == 1

test_show_warnings.py:10:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    def api_v1():
>       warnings.warn(UserWarning("api v1, should use functions from v2"))
E       UserWarning: api v1, should use functions from v2

test_show_warnings.py:5: UserWarning
========================= short test summary info ==========================
FAILED test_show_warnings.py::test_one - UserWarning: api v1, should use ...
1 failed in 0.12s

同じオプションは、pytest.ini または pyproject.toml ファイルで filterwarnings ini オプションを使用して設定できます。 たとえば、以下の構成では、すべてのユーザー警告と正規表現に一致する特定の非推奨警告を無視しますが、他のすべての警告をエラーに変換します。

# pytest.ini
[pytest]
filterwarnings =
    error
    ignore::UserWarning
    ignore:function ham\(\) is deprecated:DeprecationWarning
# pyproject.toml
[tool.pytest.ini_options]
filterwarnings = [
    "error",
    "ignore::UserWarning",
    # note the use of single quote below to denote "raw" strings in TOML
    'ignore:function ham\(\) is deprecated:DeprecationWarning',
]

警告がリスト内の複数のオプションに一致する場合、最後に一致したオプションのアクションが実行されます。

注釈

-W フラグと filterwarnings ini オプションは、構造が似た警告フィルタを使用しますが、各構成オプションはフィルタを異なる方法で解釈します。 たとえば、filterwarningsmessage は、警告メッセージの先頭が一致する必要がある正規表現を含む文字列 (大文字と小文字を区別しない) ですが、-Wmessage は、警告メッセージの先頭に含まれる必要があるリテラル文字列 (大文字と小文字を区別しない) であり、メッセージの先頭または末尾の空白を無視します。 詳細については、warning filter ドキュメントを参照してください。

@pytest.mark.filterwarnings

@pytest.mark.filterwarnings マークを使用して特定のテストアイテムに警告フィルタを追加し、テスト、クラス、さらにはモジュールレベルでキャプチャする警告をより細かく制御できます:

import warnings


def api_v1():
    warnings.warn(UserWarning("api v1, should use functions from v2"))
    return 1


@pytest.mark.filterwarnings("ignore:api v1")
def test_one():
    assert api_v1() == 1

複数のフィルタを個別のデコレータで指定できます:

# Ignore "api v1" warnings, but fail on all other warnings
@pytest.mark.filterwarnings("ignore:api v1")
@pytest.mark.filterwarnings("error")
def test_one():
    assert api_v1() == 1

重要

デコレータの順序とフィルタの優先順位に関して: デコレータは逆順で評価されるため、従来の warnings.filterwarnings() および -W option の使用と比較して、警告フィルタを逆順にリストする必要があることを覚えておくことが重要です。 これは実際には、上記の例に示されているように、以前の @pytest.mark.filterwarnings デコレータからのフィルタが後のデコレータからのフィルタよりも優先されることを意味します。

マークを使用して適用されたフィルタは、コマンドラインで渡されたフィルタや filterwarnings ini オプションで構成されたフィルタよりも優先されま��。

filterwarnings マークをクラスデコレータとして使用するか、pytestmark 変数を設定してモジュール内のすべてのテストにフィルタを適用できます:

# turns all warnings into errors for this module
pytestmark = pytest.mark.filterwarnings("error")

注釈

複数のフィルタを適用する場合 (filterwarnings マークのリストを pytestmark に割り当てることによって) 、従来の warnings.filterwarnings() の順序付けアプローチ (後のフィルタが優先される) を使用する必要があります。 これは、上記のデコレータアプローチの逆です。

pytest-warnings プラグインのリファレンス実装に対する Florian Schulze の功績に感謝します

警告サマリーの無効化

推奨されませんが、--disable-warnings コマンドラインオプションを使用して、テスト実行出力から警告サマリーを完全に抑制することができます。

警告キャプチャを完全に無効にする

このプラグインはデフォルトで有効になっていますが、pytest.ini ファイルで完全に無効にすることができます:

[pytest]
addopts = -p no:warnings

または、コマンドラインで -p no:warnings を渡します。 これは、テストスイートが外部システムを使用して警告を処理する場合に役立つ場合があります。

DeprecationWarning と PendingDeprecationWarning

デフォルトでは、pytest は PEP 565 の推奨に従って、ユーザーコードおよびサードパーティライブラリからの DeprecationWarning および PendingDeprecationWarning 警告を表示します。 これにより、ユーザーはコードを最新の状態に保ち、非推奨の警告が実際に削除されたときに発生する破損を回避できます。

ただし、ユーザーがテストで pytest.warns()pytest.deprecated_call()、または recwarn フィクスチャを使用して任意の種類の警告をキャプチャする特定のケースでは、警告はまったく表示されません。

コード内で発生する特定の非推奨警告 (サードパーティライブラリなど) を非表示にすることが有用な場合があります。 その場合、警告フィルタオプション (ini またはマーク) を使用してそれらの警告を無視することができます。

例えば:

[pytest]
filterwarnings =
    ignore:.*U.*mode is deprecated:DeprecationWarning

これにより、メッセージの先頭が正規表現 ".*U.*mode is deprecated" に一致するすべての DeprecationWarning タイプの警告が無視されます。

詳細な例については、@pytest.mark.filterwarnings および Controlling warnings を参照してください。

注釈

警告がインタープリターレベルで構成されている場合、PYTHONWARNINGS 環境変数または -W コマンドラインオプションを使用して、pytest はデフォルトでフィルタを構成しません。

また、pytest はすべての警告フィルタをリセットするという PEP 506 の提案に従いません。 これは、warnings.simplefilter() を呼び出して警告フィルタを自分で構成するテストスイートを壊す可能性があるためです (その例については #2430 を参照してください) 。

コードが非推奨警告をトリガーすることを確認する

特定の関数呼び出しが DeprecationWarning または PendingDeprecationWarning をトリガーすることを確認するために pytest.deprecated_call() を使用することもできます:

import pytest


def test_myfunction_deprecated():
    with pytest.deprecated_call():
        myfunction(17)

このテストは、myfunction17 引数で呼び出されたときに非推奨警告を発行しない場合に失敗します。

warns 関数を使用して警告をアサートする

pytest.warns() を使用してコードが特定の警告を発生させることを確認できます。 これは raises と同様の方法で動作します (ただし、raises はすべての例外をキャプチャするわけではなく、expected_exception のみをキャプチャします) :

import warnings

import pytest


def test_warning():
    with pytest.warns(UserWarning):
        warnings.warn("my warning", UserWarning)

問題の警告が発生しない場合、テストは失敗します。 キーワード引数 match を使用して、警告がテキストまたは正規表現に一致することをアサートします。 (. などの正規表現メタ文字を含む可能性のあるリテラル文字列に一致させるには、パターンを最初に re.escape でエスケープできます。

いくつかの例:

>>> with warns(UserWarning, match="must be 0 or None"):
...     warnings.warn("value must be 0 or None", UserWarning)
...

>>> with warns(UserWarning, match=r"must be \d+$"):
...     warnings.warn("value must be 42", UserWarning)
...

>>> with warns(UserWarning, match=r"must be \d+$"):
...     warnings.warn("this is not here", UserWarning)
...
Traceback (most recent call last):
  ...
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...

>>> with warns(UserWarning, match=re.escape("issue with foo() func")):
...     warnings.warn("issue with foo() func")
...

関数またはコード文字列に対して pytest.warns() を呼び出すこともできます:

pytest.warns(expected_warning, func, *args, **kwargs)
pytest.warns(expected_warning, "func(*args, **kwargs)")

関数は、発生したすべての警告のリスト (warnings.WarningMessage オブジェクトとして) も返します。 これにより、追加情報を照会できます:

with pytest.warns(RuntimeWarning) as record:
    warnings.warn("another warning", RuntimeWarning)

# check that only one warning was raised
assert len(record) == 1
# check that the message matches
assert record[0].message.args[0] == "another warning"

または、recwarn フィクスチャを使用して発生した警告を詳細に調べることができます (below を参照) 。

recwarn フィクスチャは、テストの最後に警告フィルタをリセットすることを自動的に保証するため、グローバルな状態が漏洩することはありません。

警告の記録

pytest.warns() コンテキストマネージャーを使用するか、recwarn フィクスチャを使用して発生した警告を記録できます。

pytest.warns() を使用して警告に関するアサーションを行わずに記録するには、予期される警告タイプとして引数を渡さず、デフォルトで一般的な警告に設定します:

with pytest.warns() as record:
    warnings.warn("user", UserWarning)
    warnings.warn("runtime", RuntimeWarning)

assert len(record) == 2
assert str(record[0].message) == "user"
assert str(record[1].message) == "runtime"

recwarn フィクスチャは、関数全体の警告を記録します:

import warnings


def test_hello(recwarn):
    warnings.warn("hello", UserWarning)
    assert len(recwarn) == 1
    w = recwarn.pop(UserWarning)
    assert issubclass(w.category, UserWarning)
    assert str(w.message) == "hello"
    assert w.filename
    assert w.lineno

recwarn フィクスチャと pytest.warns() コンテキストマネージャーの両方が、記録された警告に対して同じインターフェースを返します: WarningsRecorder インスタンス。 記録された警告を表示するには、このインスタンスを反復処理するか、len を呼び出して記録された警告の数を取得するか、インデックスを使用して特定の記録された警告を取得します。

テストにおける警告の追加使用例

テストでよく発生する警告に関連するいくつかの使用例と、それらに対処する方法の提案を示します:

  • 指定された警告の 少なくとも1つ が発行されることを確認するには、次を使用します:

def test_warning():
    with pytest.warns((RuntimeWarning, UserWarning)):
        ...
  • 特定の警告 のみ が発行されることを確認するには、次を使用します:

def test_warning(recwarn):
    ...
    assert len(recwarn) == 1
    user_warning = recwarn.pop(UserWarning)
    assert issubclass(user_warning.category, UserWarning)
  • 警告が まったく 発行されないことを確認するには、次を使用します:

def test_warning():
    with warnings.catch_warnings():
        warnings.simplefilter("error")
        ...
  • 警告を抑制するには、次を使用します:

with warnings.catch_warnings():
    warnings.simplefilter("ignore")
    ...

カスタム失敗メッセージ

警告を記録することで、警告が発行されない場合や他の条件が満たされた場合にカスタムテスト失敗メッセージを生成する機会が得られます。

def test():
    with pytest.warns(Warning) as record:
        f()
        if not record:
            pytest.fail("Expected a warning!")

f を呼び出したときに警告が発行されない場合、not recordTrue と評価されます。 その後、カスタムエラーメッセージを使用して pytest.fail() を呼び出すことができます。

内部 pytest 警告

pytest は、不適切な使用法や非推奨の機能など、いくつかの状況で独自の警告を生成する場合があります。

たとえば、pytest は python_classes に一致するクラスに遭遇し、__init__ コンストラクタも定義されている場合、クラスのインスタンス化を防ぐため、警告を発します:

# content of test_pytest_warnings.py
class Test:
    def __init__(self):
        pass

    def test_foo(self):
        assert 1 == 1
$ pytest test_pytest_warnings.py -q

============================= warnings summary =============================
test_pytest_warnings.py:1
  /home/sweet/project/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor (from: test_pytest_warnings.py)
    class Test:

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
1 warning in 0.12s

これらの警告は、他の種類の警告をフィルタリングするために使用される同じ組み込みメカニズムを使用してフィルタリングされる場合があります。

機能の非推奨化と最終的な削除の進め方については、後方互換性ポリシー をお読みください。

警告の完全なリストは、the reference documentation に記載されています。

リソース警告

tracemalloc モジュールが有効になっている場合、pytest によってキャプチャされたときに ResourceWarning のソースに関する追加情報を取得できます。

テストを実行するときに tracemalloc を有効にする便利な方法の1つは、PYTHONTRACEMALLOC を十分な数のフレーム (たとえば 20、ただしその数はアプリケーションに依存します) に設定することです。

詳細については、Python ドキュメントの Python Development Mode セクションを参照してください。