テストでアサーションを記述および報告する方法

assert ステートメントによるアサーション

pytest を使用すると、Python テストで期待値や値を検証するために標準の Python assert を使用できます。 例えば、次のように記述できます:

# content of test_assert1.py
def f():
    return 3


def test_function():
    assert f() == 4

関数が特定の値を返すことをアサートします。 このアサーションが失敗すると、関数呼び出しの戻り値が表示されます:

$ pytest test_assert1.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_assert1.py F                                                    [100%]

================================= FAILURES =================================
______________________________ test_function _______________________________

    def test_function():
>       assert f() == 4
E       assert 3 == 4
E        +  where 3 = f()

test_assert1.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_assert1.py::test_function - assert 3 == 4
============================ 1 failed in 0.12s =============================

pytest は、呼び出し、属性、比較、および二項演算子と単項演算子を含む最も一般的な部分式の値を表示するサポートを提供します (pytest による Python の失敗レポートのデモ を参照) 。 これにより、ボイラープレートコードを使用せずに慣用的な Python 構文を使用しながら、イントロスペクション情報を失うことはありません。

このようにアサーションにメッセージが指定されている場合:

assert a % 2 == 0, "value was odd, should be even"

それはトレースバックのアサーションイントロスペクションと一緒に印刷されます。

アサーションイントロスペクションの詳細については アサーションイントロスペクションの詳細 を参照してください。

予期される例外に関するアサーション

発生した例外に関するアサーションを記述するために、次のように pytest.raises() をコンテキストマネージャとして使用できます:

import pytest


def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

実際の例外情報にアクセスする必要がある場合は、次のように使用できます:

def test_recursion_depth():
    with pytest.raises(RuntimeError) as excinfo:

        def f():
            f()

        f()
    assert "maximum recursion" in str(excinfo.value)

excinfoExceptionInfo インスタンスであり、実際に発生した例外のラッパーです。 主な関心のある属性は .type.value、および .traceback です。

pytest.raises は例外タイプまたはそのサブクラス (標準の except ステートメントのように) に一致することに注意してください。 コードブロックが正確な例外タイプを発生させているかどうかを確認する場合は、それを明示的に確認する必要があります:

def test_foo_not_implemented():
    def foo():
        raise NotImplementedError

    with pytest.raises(RuntimeError) as excinfo:
        foo()
    assert excinfo.type is RuntimeError

pytest.raises() の呼び出しは成功しますが、関数が NotImplementedError を発生させるため、NotImplementedErrorRuntimeError のサブクラスです。 ただし、次の assert ステートメントは問題をキャッチします。

例外メッセージの一致

コンテキストマネージャに match キーワードパラメータを渡して、正規表現が例外の文字列表現に一致するかどうかをテストできます (unittestTestCase.assertRaisesRegex メソッドに似ています) :

import pytest


def myfunc():
    raise ValueError("Exception 123 raised")


def test_match():
    with pytest.raises(ValueError, match=r".* 123 .*"):
        myfunc()

注意:

  • match パラメータは re.search() 関数と一致するため、上記の例では match='123' も機能します。

  • match パラメータは PEP-678 __notes__ にも一致します。

例外グループの一致

excinfo.group_contains() メソッドを使用して、ExceptionGroup の一部として返される例外をテストすることもできます:

def test_exception_in_group():
    with pytest.raises(ExceptionGroup) as excinfo:
        raise ExceptionGroup(
            "Group message",
            [
                RuntimeError("Exception 123 raised"),
            ],
        )
    assert excinfo.group_contains(RuntimeError, match=r".* 123 .*")
    assert not excinfo.group_contains(TypeError)

オプションの match キーワードパラメータは、pytest.raises() と同じように機能します。

デフォルトでは、group_contains() はネストされた ExceptionGroup インスタンスの任意のレベルで一致する例外を再帰的に検索します。 特定のレベルでのみ例外を一致させたい場合は、depth キーワードパラメータを指定できます。 最上位の ExceptionGroup に直接含まれる例外は depth=1 と一致します。

def test_exception_in_group_at_given_depth():
    with pytest.raises(ExceptionGroup) as excinfo:
        raise ExceptionGroup(
            "Group message",
            [
                RuntimeError(),
                ExceptionGroup(
                    "Nested group",
                    [
                        TypeError(),
                    ],
                ),
            ],
        )
    assert excinfo.group_contains(RuntimeError, depth=1)
    assert excinfo.group_contains(TypeError, depth=2)
    assert not excinfo.group_contains(RuntimeError, depth=2)
    assert not excinfo.group_contains(TypeError, depth=1)

代替形式 (レガシー)

実行される関数を *args および **kwargs とともに渡し、pytest.raises() が引数で関数を実行し、指定された例外が発生することをアサートする代替形式があります:

def func(x):
    if x <= 0:
        raise ValueError("x needs to be larger than zero")


pytest.raises(ValueError, func, x=-1)

レポーターは、例外なし間違った例外 などの失敗の場合に役立つ出力を提供します。

この形式は、Python 言語に with ステートメントが追加される前に開発された元の pytest.raises() API でした。 現在では、この形式はほとんど使用されておらず、コンテキストマネージャ形式 (with を使用) がより読みやすいと考えられています。 それにもかかわらず、この形式は完全にサポートされており、非推奨ではありません。

xfail マークと pytest.raises

pytest.mark.xfailraises 引数を指定することも可能で、これにより、テストが単に例外を発生させるだけでなく、より具体的な方法で失敗していることを確認できます:

def f():
    raise IndexError()


@pytest.mark.xfail(raises=IndexError)
def test_f():
    f()

これは、テストが IndexError またはそのサブクラスを発生させることによって失敗した場合にのみ "xfail" します。

  • raises パラメータを使用した pytest.mark.xfail の使用は、おそらく未修正のバグ (テストが「起こるべきこと」を説明する場合) や依存関係のバグを文書化するようなものに適しています。

  • pytest.raises() を使用する方が、自分のコードが意図的に発生させている例外をテストする場合 (ほとんどの場合) に適している可能性があります。

予期される警告に関するアサーション

pytest.warns を使用して、コードが特定の警告を発生させることを確認できます。

コンテキストに応じた比較の利用

pytest は、比較に遭遇したときにコンテキストに応じた情報を提供するための豊富なサポートを提供します。 例えば:

# content of test_assert2.py
def test_set_comparison():
    set1 = set("1308")
    set2 = set("8035")
    assert set1 == set2

このモジュールを実行すると:

$ pytest test_assert2.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_assert2.py F                                                    [100%]

================================= FAILURES =================================
___________________________ test_set_comparison ____________________________

    def test_set_comparison():
        set1 = set("1308")
        set2 = set("8035")
>       assert set1 == set2
E       AssertionError: assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'}
E
E         Extra items in the left set:
E         '1'
E         Extra items in the right set:
E         '5'
E         Use -v to get more diff

test_assert2.py:4: AssertionError
========================= short test summary info ==========================
FAILED test_assert2.py::test_set_comparison - AssertionError: assert {'0'...
============================ 1 failed in 0.12s =============================

特別な比較は、いくつかのケースで行われます:

  • 長い文字列の比較: コンテキスト差分が表示されます

  • 長いシーケンスの比較: 最初の失敗インデックス

  • 辞書の比較: 異なるエントリ

さらに多くの例については、reporting demo を参照してください。

失敗したアサーションのための独自の説明を定義する

pytest_assertrepr_compare フックを実装することで、独自の詳細な説明を追加することができます。

pytest_assertrepr_compare(config, op, left, right)[ソース]

失敗したアサーション式の比較に対する説明を返します。

カスタム説明がない場合は None を返し、それ以外の場合は文字列のリストを返します。 文字列は改行で結合されますが、文字列内の改行はエスケープされます。 最初の行以外はすべて少しインデントされることに注意してください。 最初の行は要約の意図です。

パラメータ:
  • config (Config) -- pytest の設定オブジェクト。

  • op (str) -- 演算子、例: "==""!=""not in"

  • left (object) -- 左オペランド。

  • right (object) -- 右オペランド。

conftest プラグインで使用

任意の conftest ファイルがこのフックを実装できます。 特定のアイテムに対しては、そのアイテムのディレクトリおよび親ディレクトリ内の conftest ファイルのみが参照されます。

例として、Foo オブジェクトの代替説明を提供する conftest.py ファイルに次のフックを追加することを検討してください:

# content of conftest.py
from test_foocompare import Foo


def pytest_assertrepr_compare(op, left, right):
    if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
        return [
            "Comparing Foo instances:",
            f"   vals: {left.val} != {right.val}",
        ]

次に、このテストモジュールを考えます:

# content of test_foocompare.py
class Foo:
    def __init__(self, val):
        self.val = val

    def __eq__(self, other):
        return self.val == other.val


def test_compare():
    f1 = Foo(1)
    f2 = Foo(2)
    assert f1 == f2

テストモジュールを実行して、conftest ファイルに定義されたカスタム出力を取得できます:

$ pytest -q test_foocompare.py
F                                                                    [100%]
================================= FAILURES =================================
_______________________________ test_compare _______________________________

    def test_compare():
        f1 = Foo(1)
        f2 = Foo(2)
>       assert f1 == f2
E       assert Comparing Foo instances:
E            vals: 1 != 2

test_foocompare.py:12: AssertionError
========================= short test summary info ==========================
FAILED test_foocompare.py::test_compare - assert Comparing Foo instances:
1 failed in 0.12s

アサーションイントロスペクションの詳細

失敗したアサーションに関する詳細を報告するには、アサーションステートメントを実行する前に書き換えます。 書き換えられたアサーションステートメントは、アサーション失敗メッセージにイントロスペクション情報を入れます。 pytest はテスト収集プロセスによって直接検出されたテストモジュールのみを書き換えるため、テストモジュール自体ではないサポートモジュールのアサーションは書き換えられません

インポートする前に register_assert_rewrite を呼び出すことで、インポートされたモジュールのアサーション書き換えを手動で有効にできます (それを行うのに適した場所は、ルートの conftest.py です) 。

詳細については、Benjamin Peterson が書いた pytest の新しいアサーション書き換えの舞台裏 を参照してください。

アサーション書き換えはディスク上にファイルをキャッシュします

pytest は、書き換えられたモジュールをキャッシュのためにディスクに書き戻します。 この動作を無効にするには (たとえば、ファイルを頻繁に移動するプロジェクトで古い .pyc ファイルを残さないようにするため) 、これを conftest.py ファイルの先頭に追加します:

import sys

sys.dont_write_bytecode = True

アサーションイントロスペクションの利点は引き続き得られますが、唯一の変更点は .pyc ファイルがディスクにキャッシュされないことです。

さらに、新しい .pyc ファイルを書き込めない場合 (読み取り専用ファイルシステムや zip ファイル内など) 、書き換えはキャッシュを静かにスキップします。

アサーション書き換えの無効化

pytest は、新しい pyc ファイルを書き込むためにインポートフックを使用してインポート時にテストモジュールを書き換えます。 ほとんどの場合、これは透過的に機能します。 ただし、インポート機構を自分で操作している場合、インポートフックが干渉する可能性があります。

この場合、2 つのオプションがあります:

  • 特定のモジュールの書き換えを無効にするには、そのドキュメント文字列に PYTEST_DONT_REWRITE という文字列を追加します。

  • すべてのモジュールの書き換えを無効にするには、--assert=plain を使用します。