Wie man Assertions in Tests schreibt und meldet

Assertions mit der assert-Anweisung

pytest erlaubt Ihnen die Verwendung der Standard-Python-Anweisung assert zur Überprüfung von Erwartungen und Werten in Python-Tests. Zum Beispiel können Sie Folgendes schreiben:

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


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

um zu überprüfen, ob Ihre Funktion einen bestimmten Wert zurückgibt. Wenn diese Assertion fehlschlägt, sehen Sie den Rückgabewert des Funktionsaufrufs.

$ pytest test_assert1.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.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 unterstützt die Anzeige der Werte der gängigsten Unterausdrücke, einschließlich Aufrufe, Attribute, Vergleiche sowie binäre und unäre Operatoren. (Siehe Demo von Python-Fehlermeldungen mit pytest). Dies ermöglicht es Ihnen, idiomatische Python-Konstrukte ohne Boilerplate-Code zu verwenden, ohne dabei auf Introspektionsinformationen zu verzichten.

Wenn eine Nachricht mit der Assertion wie folgt angegeben wird:

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

wird sie zusammen mit der Assertion-Introspektion im Traceback ausgegeben.

Siehe Details zur Assertion-Introspektion für weitere Informationen zur Assertion-Introspektion.

Assertions über ungefähre Gleichheit

Beim Vergleichen von Fließkommawerten (oder Arrays von Fließkommazahlen) sind kleine Rundungsfehler üblich. Anstatt assert abs(a - b) < tol oder numpy.isclose zu verwenden, können Sie pytest.approx() verwenden.

import pytest
import numpy as np


def test_floats():
    assert (0.1 + 0.2) == pytest.approx(0.3)


def test_arrays():
    a = np.array([1.0, 2.0, 3.0])
    b = np.array([0.9999, 2.0001, 3.0])
    assert a == pytest.approx(b)

pytest.approx funktioniert mit Skalaren, Listen, Dictionaries und NumPy-Arrays. Es unterstützt auch Vergleiche, die NaNs beinhalten.

Siehe pytest.approx() für Details.

Assertions über erwartete Ausnahmen

Um Assertions über ausgelöste Ausnahmen zu schreiben, können Sie pytest.raises() als Kontextmanager verwenden, wie hier:

import pytest


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

und wenn Sie Zugriff auf die tatsächlichen Ausnahmeinformationen benötigen, können Sie verwenden:

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

        def f():
            f()

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

excinfo ist eine ExceptionInfo-Instanz, die ein Wrapper um die tatsächlich ausgelöste Ausnahme ist. Die wichtigsten Attribute von Interesse sind .type, .value und .traceback.

Beachten Sie, dass pytest.raises den Ausnahmetyp oder alle Unterklassen (wie die Standard-`except`-Anweisung) abgleicht. Wenn Sie überprüfen möchten, ob ein Codeblock einen exakten Ausnahmetyp auslöst, müssen Sie dies explizit überprüfen:

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

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

Der Aufruf pytest.raises() wird erfolgreich sein, obwohl die Funktion NotImplementedError auslöst, da NotImplementedError eine Unterklasse von RuntimeError ist; die folgende `assert`-Anweisung wird jedoch das Problem erfassen.

Abgleichen von Ausnahmemeldungen

Sie können einen Schlüsselwortparameter match an den Kontextmanager übergeben, um zu testen, ob ein regulärer Ausdruck mit der String-Darstellung einer Ausnahme übereinstimmt (ähnlich der Methode TestCase.assertRaisesRegex aus unittest):

import pytest


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


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

Hinweise

  • Der Parameter match wird mit der Funktion re.search() abgeglichen, daher hätte im obigen Beispiel match='123' ebenfalls funktioniert.

  • Der Parameter match gleicht auch gegen die PEP-678 `__notes__` ab.

Assertions über erwartete Ausnahmegruppen

Wenn eine BaseExceptionGroup oder ExceptionGroup erwartet wird, können Sie pytest.RaisesGroup verwenden.

def test_exception_in_group():
    with pytest.RaisesGroup(ValueError):
        raise ExceptionGroup("group msg", [ValueError("value msg")])
    with pytest.RaisesGroup(ValueError, TypeError):
        raise ExceptionGroup("msg", [ValueError("foo"), TypeError("bar")])

Er akzeptiert einen Parameter match, der gegen die Gruppenmeldung prüft, und einen Parameter check, der einen beliebigen aufrufbaren Parameter nimmt, dem er die Gruppe übergibt, und nur erfolgreich ist, wenn der Aufrufbare True zurückgibt.

def test_raisesgroup_match_and_check():
    with pytest.RaisesGroup(BaseException, match="my group msg"):
        raise BaseExceptionGroup("my group msg", [KeyboardInterrupt()])
    with pytest.RaisesGroup(
        Exception, check=lambda eg: isinstance(eg.__cause__, ValueError)
    ):
        raise ExceptionGroup("", [TypeError()]) from ValueError()

Im Gegensatz zu `except*` ist er streng in Bezug auf Struktur und nicht verpackte Ausnahmen, daher sollten Sie möglicherweise die Parameter flatten_subgroups und/oder allow_unwrapped setzen.

def test_structure():
    with pytest.RaisesGroup(pytest.RaisesGroup(ValueError)):
        raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
    with pytest.RaisesGroup(ValueError, flatten_subgroups=True):
        raise ExceptionGroup("1st group", [ExceptionGroup("2nd group", [ValueError()])])
    with pytest.RaisesGroup(ValueError, allow_unwrapped=True):
        raise ValueError

Um weitere Details zu den enthaltenen Ausnahmen anzugeben, können Sie pytest.RaisesExc verwenden.

def test_raises_exc():
    with pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="foo")):
        raise ExceptionGroup("", (ValueError("foo")))

Beide stellen eine Methode pytest.RaisesGroup.matches() pytest.RaisesExc.matches() bereit, wenn Sie Abgleiche außerhalb der Verwendung als Kontextmanager durchführen möchten. Dies kann hilfreich sein, wenn Sie `.__context__` oder `.__cause__` überprüfen.

def test_matches():
    exc = ValueError()
    exc_group = ExceptionGroup("", [exc])
    if RaisesGroup(ValueError).matches(exc_group):
        ...
    # helpful error is available in `.fail_reason` if it fails to match
    r = RaisesExc(ValueError)
    assert r.matches(e), r.fail_reason

Sehen Sie sich die Dokumentation zu pytest.RaisesGroup und pytest.RaisesExc für weitere Details und Beispiele an.

ExceptionInfo.group_contains()

Warnung

Diese Hilfsfunktion erleichtert die Überprüfung auf das Vorhandensein bestimmter Ausnahmen, ist aber sehr schlecht geeignet, um zu überprüfen, ob die Gruppe *keine* anderen Ausnahmen enthält. Daher wird dies erfolgreich sein:

class EXTREMELYBADERROR(BaseException):
    """This is a very bad error to miss"""


def test_for_value_error():
    with pytest.raises(ExceptionGroup) as excinfo:
        excs = [ValueError()]
        if very_unlucky():
            excs.append(EXTREMELYBADERROR())
        raise ExceptionGroup("", excs)
    # This passes regardless of if there's other exceptions.
    assert excinfo.group_contains(ValueError)
    # You can't simply list all exceptions you *don't* want to get here.

Es gibt keine gute Möglichkeit, excinfo.group_contains() zu verwenden, um sicherzustellen, dass Sie *keine* anderen Ausnahmen als die erwartete erhalten. Sie sollten stattdessen pytest.RaisesGroup verwenden, siehe Assertions über erwartete Ausnahmegruppen.

Sie können auch die Methode excinfo.group_contains() verwenden, um nach Ausnahmen zu suchen, die als Teil einer ExceptionGroup zurückgegeben werden.

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)

Der optionale Schlüsselwortparameter match funktioniert genauso wie bei pytest.raises().

Standardmäßig sucht group_contains() rekursiv nach einer passenden Ausnahme auf jeder Ebene verschachtelter ExceptionGroup-Instanzen. Sie können einen Schlüsselwortparameter depth angeben, wenn Sie nur eine Ausnahme auf einer bestimmten Ebene abgleichen möchten; Ausnahmen, die direkt in der obersten ExceptionGroup enthalten sind, würden depth=1 entsprechen.

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)

Alternative pytest.raises-Form (Legacy)

Es gibt eine alternative Form von pytest.raises(), bei der Sie eine Funktion übergeben, die zusammen mit `*args` und `**kwargs` ausgeführt wird. pytest.raises() führt dann die Funktion mit diesen Argumenten aus und stellt sicher, dass die angegebene Ausnahme ausgelöst wird.

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


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

Der Reporter liefert Ihnen hilfreiche Ausgaben bei Fehlern wie *keine Ausnahme* oder *falsche Ausnahme*.

Diese Form war die ursprüngliche pytest.raises()-API, die entwickelt wurde, bevor die `with`-Anweisung zur Python-Sprache hinzugefügt wurde. Heutzutage wird diese Form selten verwendet, wobei die Kontextmanager-Form (mit `with`) als lesbarer gilt. Dennoch ist diese Form vollständig unterstützt und in keiner Weise veraltet.

xfail-Markierung und pytest.raises

Es ist auch möglich, ein Argument raises an pytest.mark.xfail zu übergeben, das prüft, ob der Test auf eine spezifischere Weise fehlschlägt, als nur eine Ausnahme auszulösen.

def f():
    raise IndexError()


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

Dies wird nur "xfailen", wenn der Test durch Auslösen von IndexError oder Unterklassen fehlschlägt.

  • Die Verwendung von pytest.mark.xfail mit dem Parameter raises ist wahrscheinlich besser für Dinge wie die Dokumentation von ungelösten Fehlern (wo der Test beschreibt, was passieren "sollte") oder Fehler in Abhängigkeiten.

  • Die Verwendung von pytest.raises() ist wahrscheinlich besser für Fälle, in denen Sie Ausnahmen testen, die Ihr eigener Code absichtlich auslöst, was in den meisten Fällen zutrifft.

Sie können auch pytest.RaisesGroup verwenden.

def f():
    raise ExceptionGroup("", [IndexError()])


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

Assertions über erwartete Warnungen

Sie können überprüfen, ob Code eine bestimmte Warnung auslöst, indem Sie pytest.warns verwenden.

Verwendung kontextsensitiver Vergleiche

pytest bietet eine umfassende Unterstützung für die Bereitstellung kontextsensibler Informationen, wenn es auf Vergleiche stößt. Zum Beispiel:

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

Wenn Sie dieses Modul ausführen:

$ pytest test_assert2.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-9.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 =============================

Spezielle Vergleiche werden für eine Reihe von Fällen durchgeführt:

  • Vergleich langer Strings: Es wird eine Kontext-Diff angezeigt.

  • Vergleich langer Sequenzen: Erste fehlerhafte Indizes.

  • Vergleich von Dictionaries: Unterschiedliche Einträge.

Sehen Sie sich die Reporting-Demo für viele weitere Beispiele an.

Eigene Erklärungen für fehlgeschlagene Assertions definieren

Es ist möglich, eigene detaillierte Erklärungen hinzuzufügen, indem der Hook pytest_assertrepr_compare implementiert wird.

pytest_assertrepr_compare(config, op, left, right)[Quelle]

Gibt eine Erklärung für Vergleiche in fehlgeschlagenen Assertionsausdrücken zurück.

Gibt None zurück, wenn keine benutzerdefinierte Erklärung vorhanden ist, andernfalls eine Liste von Strings. Die Strings werden durch Zeilenumbrüche verbunden, aber alle Zeilenumbrüche *in* einem String werden maskiert. Beachten Sie, dass alle Zeilen außer der ersten leicht eingerückt werden, die Absicht ist, dass die erste Zeile eine Zusammenfassung ist.

Parameter:
  • config (Config) – Das pytest Config-Objekt.

  • op (str) – Der Operator, z.B. "==", "!=", "not in".

  • left (object) – Der linke Operand.

  • right (object) – Der rechte Operand.

Verwendung in conftest-Plugins

Jede conftest-Datei kann diesen Hook implementieren. Für ein bestimmtes Element werden nur conftest-Dateien im Verzeichnis des Elements und in seinen übergeordneten Verzeichnissen konsultiert.

Als Beispiel betrachten Sie das Hinzufügen des folgenden Hooks in einer conftest.py-Datei, die eine alternative Erklärung für Foo-Objekte liefert:

# 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}",
        ]

Jetzt, bei diesem Testmodul:

# 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

Sie können das Testmodul ausführen und die benutzerdefinierte Ausgabe erhalten, die in der conftest-Datei definiert ist.

$ 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

Rückgabe von Nicht-None-Werten in Testfunktionen

Eine pytest.PytestReturnNotNoneWarning wird ausgegeben, wenn eine Testfunktion einen anderen Wert als None zurückgibt.

Dies hilft, einen häufigen Fehler zu vermeiden, der von Anfängern gemacht wird, die davon ausgehen, dass die Rückgabe eines `bool` (z.B. `True` oder `False`) bestimmt, ob ein Test erfolgreich ist oder fehlschlägt.

Beispiel

@pytest.mark.parametrize(
    ["a", "b", "result"],
    [
        [1, 2, 5],
        [2, 3, 8],
        [5, 3, 18],
    ],
)
def test_foo(a, b, result):
    return foo(a, b) == result  # Incorrect usage, do not do this.

Da pytest Rückgabewerte ignoriert, kann es überraschend sein, dass der Test niemals auf der Grundlage des zurückgegebenen Werts fehlschlägt.

Die korrigierte Lösung ist, die `return`-Anweisung durch eine `assert`-Anweisung zu ersetzen:

@pytest.mark.parametrize(
    ["a", "b", "result"],
    [
        [1, 2, 5],
        [2, 3, 8],
        [5, 3, 18],
    ],
)
def test_foo(a, b, result):
    assert foo(a, b) == result

Details zur Assertion-Introspektion

Die Berichterstattung von Details über eine fehlgeschlagene Assertion wird durch die Umschreibung von assert-Anweisungen erreicht, bevor sie ausgeführt werden. Umgeschriebene assert-Anweisungen fügen dem Assertion-Fehlermeldungs-Text Introspektionsinformationen hinzu. pytest schreibt nur Testmodule um, die direkt von seinem Test-Sammlungsprozess entdeckt werden, daher werden **Asserts in unterstützenden Modulen, die selbst keine Testmodule sind, nicht umgeschrieben**.

Sie können die Assertion-Umschreibung für ein importiertes Modul manuell aktivieren, indem Sie register_assert_rewrite aufrufen, bevor Sie es importieren (ein guter Ort dafür ist Ihre Stammdatei `conftest.py`).

Für weitere Informationen schrieb Benjamin Peterson Hinter den Kulissen der neuen Assertion-Umschreibung von pytest.

Assertion-Umschreibung speichert Dateien auf der Festplatte

pytest schreibt die umgeschriebenen Module zur Zwischenspeicherung auf die Festplatte zurück. Sie können dieses Verhalten deaktivieren (z.B. um veraltete `.pyc`-Dateien in Projekten zu vermeiden, die Dateien häufig verschieben), indem Sie Folgendes am Anfang Ihrer conftest.py-Datei hinzufügen:

import sys

sys.dont_write_bytecode = True

Beachten Sie, dass Sie immer noch die Vorteile der Assertion-Introspektion erhalten, die einzige Änderung ist, dass die `.pyc`-Dateien nicht auf der Festplatte zwischengespeichert werden.

Darüber hinaus überspringt die Umschreibung stillschweigend die Zwischenspeicherung, wenn sie keine neuen `.pyc`-Dateien schreiben kann, z.B. auf einem schreibgeschützten Dateisystem oder in einer ZIP-Datei.

Deaktivieren der Assert-Umschreibung

pytest schreibt Testmodule beim Importieren um, indem es einen Import-Hook verwendet, um neue `pyc`-Dateien zu schreiben. Meistens funktioniert dies transparent. Wenn Sie jedoch selbst mit der Import-Maschinerei arbeiten, kann der Import-Hook stören.

Wenn dies der Fall ist, haben Sie zwei Möglichkeiten:

  • Schalten Sie die Umschreibung für ein bestimmtes Modul aus, indem Sie den String PYTEST_DONT_REWRITE zu dessen Docstring hinzufügen.

  • Schalten Sie die Umschreibung für alle Module aus, indem Sie --assert=plain verwenden.