Parametrisieren mit Pytest
Das Parametrisierungsfeature in Pytest mag anfangs komplex erscheinen, aber sein Zweck ist einfach, sobald Sie das Problem verstehen, das es anhebt. Im Wesentlichen ermöglicht es Ihnen parametrize, dieselbe Testfunktion effizient mit unterschiedlichen Eingaben auszuführen, sodass detaillierte und vielfältige Überprüfungen mit weniger Code durchgeführt werden können.
Beim Aufrufen der Parametrisierung ist das erste Argument eine Zeichenfolge, die einen oder mehrere Argumentnamen enthält, "test\_input_"z. B. . Das zweite Argument enthält eine Liste von Argumentwerten, ["27", "6+9", "0", "O"]z. B. . . Die letzten vier Argumente weisen Standardwerte auf und sind optional.
Sie können die englischsprachige Pytest-API-Referenz für parametrize hier finden: pytest.Metafunc.parametrize.
Wann man Parametrisierung verwenden sollte
Zwei häufige Szenarien, in denen Sie die Parametrisierung verwenden möchten, sind:
- Beim Testen von for-Schleifen
- Wenn mehrere Tests dasselbe Verhalten bestätigen
Sehen wir uns zunächst jedes Beispiel ohne Parametrisierung an und dann mit ihr, um zu zeigen, wie sie unsere Tests verbessern kann.
For-Schleifen
Hier ist ein Beispiel für eine Testfunktion mit einer for Schleife:
def test_string_is_digit():
items = ["1", "10", "33"]
for item in items:
assert item.isdigit()
Dieser Test ist problematisch, da bei einem Fehlschlagen mehrere Probleme auftreten können, darunter:
- Mehrdeutige Testberichte: Im Testbericht wird nicht klargestellt, ob nur ein Element fehlgeschlagen ist oder mehrere Fehler auftreten.
- Einzelne Testansicht: Alle Elemente werden als einzelner Test betrachtet, wodurch die Leistung einzelner Elemente verdeckt wird.
- Unsichere Fixes: Wenn ein Fehler behoben wird, gibt es keine Möglichkeit zu wissen, ob alle Probleme behoben werden, ohne den gesamten Test erneut auszuführen.
Lassen Sie uns den Test so ändern, dass er speziell zwei Elemente enthält, von denen erwartet wird, dass sie fehlschlagen.
def test_string_is_digit():
items = ["No", "1", "10", "33", "Yes"]
for item in items:
assert item.isdigit()
Beim Ausführen des Tests wird nur ein Fehler angezeigt, obwohl in dieser Liste zwei ungültige Elemente vorhanden sind:
$ pytest test_items.py
=================================== FAILURES ===================================
_____________________________ test_string_is_digit _____________________________
test_items.py:4: in test_string_is_digit
assert item.isdigit()
E AssertionError: assert False
E + where False = <built-in method isdigit of str object at 0x103fa1df0>()
E + where <built-in method isdigit of str object at 0x103fa1df0> = 'No'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit - AssertionError: assert False
============================== 1 failed in 0.01s ===============================
Dies ist ein großartiger Anwendungsfall für die Parametrisierung. Bevor Sie sich ansehen, wie Sie den Test aktualisieren, erfahren Sie mehr über eine andere häufig auftretende Situation, die keine for-Schleifen beinhaltet.
Tests, die dasselbe Verhalten bestätigen
Eine Gruppe von Tests, die dieselbe Assertion machen, sind auch ein guter Kandidat für die Parametrisierung. Wenn der vorherige Test mit einem Test für jedes Element neu geschrieben wurde, würde es eine bessere Fehlerberichterstattung ermöglichen, aber es würde sich wiederholen:
def test_is_digit_1():
assert "1".isdigit()
def test_is_digit_10():
assert "10".isdigit()
def test_is_digit_33():
assert "33".isdigit()
Diese Tests sind besser in dem Sinne, dass ein Fehler einfach mit einer einzelnen Eingabe verbunden werden kann. Und obwohl es ungewöhnlich erscheinen mag, mehrere ähnliche Tests zu haben, ist dies häufig in Produktionstestsuites der Fall, die versuchen, möglichst präzise zu sein.
Obgleich diese Tests besser wären, da sie genau melden können, was fehlschlägt oder besteht, treten bei ihnen auch die folgenden Probleme auf:
- Code wiederholt sich, was zu einer Wartungslast führt
- Es besteht die Möglichkeit von Tippfehlern und Fehlern beim Aktualisieren der Tests.
- Aufgrund der Wiederholungen können Entwickelnde möglicherweise nicht alle Anwendungsfälle und Eingaben einbeziehen.
So verwenden Sie Parametrisieren
Nachdem Sie nun einige der Anwendungsfälle für die Parametrisierung kennen, aktualisieren wir den Test, der eine for Schleife verwendet hat, die fehlerhafte Elemente enthält.
Um parametrize verwenden zu können, müssen Sie pytest als Bibliothek importieren und dann als Dekorator in der Funktion nutzen. Hier ist der aktualisierte Test:
import pytest
@pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
def test_string_is_digit(item):
assert item.isdigit()
Bevor Sie die Tests ausführen, gehen wir die Änderungen durch.
Der pytest.mark.parametrize()-Decorator definiert zwei Argumente. Das erste Argument ist eine Zeichenfolge namens "item". Diese Zeichenfolge wird als benanntes Argument für die Testfunktion verwendet, die in der nächsten Zeile in der Definition der Testfunktion angezeigt wird. Das zweite Argument ist die Liste der Testwerte.
Umfassende Fehlerberichterstattung
Im Hintergrund betrachtet Pytest jedes Element in dieser Liste als separaten Test. Das bedeutet, dass bestandene und fehlerhafte Tests separat gemeldet werden. Schauen wir mal, was passiert, wenn der Test mit pytest ausgeführt wird.
$ pytest test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items
test_items.py F...F [100%]
=================================== FAILURES ===================================
___________________________ test_string_is_digit[No] ___________________________
test_items.py:5: in test_string_is_digit
assert item.isdigit()
E AssertionError: assert False
E + where False = <built-in method isdigit of str object at 0x102d45e30>()
E + where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
__________________________ test_string_is_digit[Yes] ___________________________
test_items.py:5: in test_string_is_digit
assert item.isdigit()
E AssertionError: assert False
E + where False = <built-in method isdigit of str object at 0x102d45df0>()
E + where <built-in method isdigit of str object at 0x102d45df0> = 'Yes'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False
FAILED test_items.py::test_string_is_digit[Yes] - AssertionError: assert False
========================= 2 failed, 3 passed in 0.07s ==========================
Es gibt einige wichtige Elemente in der Testberichterstattung. Erstens sehen wir, dass aus einem einzelnen Test mit pytest insgesamt fünf Tests gemeldet werden: drei bestanden und zwei fehlgeschlagen. Die Fehler werden separat gemeldet, einschließlich der fehlerhaften Eingabe.
$ pytest test_items.py
___________________________ test_string_is_digit[No] ___________________________
[...]
E + where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
[...]
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False
Es ist schwierig, den Wert zu verpassen, der den Fehler an so vielen Stellen verursacht hat, an denen er gemeldet wird.
Verwenden des ausführlichen Ausgabeflags
Wenn die Tests in der Befehlszeile ausgeführt werden, ist die Berichterstattung über bestandene Tests minimal. So sieht der Test nach einem Update aus, um die Fehler zu beheben:
@pytest.mark.parametrize("item", ["0", "1", "10", "33", "9"])
def test_string_is_digit(item):
assert item.isdigit()
Das Ausführen der Tests erzeugt eine minimale Ausgabe:
$ pytest test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items
test_items.py ..... [100%]
============================== 5 passed in 0.01s ===============================
Wenn Sie die Ausführlichkeit erhöhen, werden die Werte angezeigt, die pytest bei Verwendung der Parametrisierung für jeden Test ausführt:
$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items
test_items.py::test_string_is_digit[0] PASSED [ 20%]
test_items.py::test_string_is_digit[1] PASSED [ 40%]
test_items.py::test_string_is_digit[10] PASSED [ 60%]
test_items.py::test_string_is_digit[33] PASSED [ 80%]
test_items.py::test_string_is_digit[9] PASSED [100%]
============================== 5 passed in 0.01s ===============================
Verwenden mehrerer Argumentnamen
Die Beispiele, die wir bisher gesehen haben, haben nur einen Argumentnamen im ersten Argument. Wir haben verwendet "item" , aber Sie können mehrere Argumentnamen in die Zeichenfolge einfügen, die das erste Argument durch Kommas getrennt angibt.
Ein Anwendungsfall für die Verwendung mehrerer Argumentnamen besteht darin, dass Sie eine Reihe erwarteter Werte übergeben möchten, die anhand Ihres Eingabewerts getestet werden sollen. In Ihrem zweiten Argument muss jedes Element in Ihrer Gruppe eine Menge von Werten aufweisen, die der Anzahl der Eingabenamen entsprechen. Wenn Ihre Eingabenamen beispielsweise sind "test\_input, expected\_value", könnte Ihr zweites Argument etwa wie folgt aussehen: [("3+5", 8), ("3*4", 12)]
Dieser Test überprüft, ob ein Objekt über ein Attribut mit der Python-Funktion hasattr() verfügt. Es gibt einen booleschen Wert zurück, je nachdem, ob das Objekt über das zugeordnete Attribut verfügt.
>>> hasattr(dict(), "keys")
True
>>> hasattr("string", "append")
False
Da hasattr() zwei Argumente erforderlich sind, können wir parametrisieren wie folgt verwenden:
@pytest.mark.parametrize("item, attribute", [("", "format"), (list(), "append")])
def test_attributes(item, attribute):
assert hasattr(item, attribute)
Der Parametrisierungsdekoror verwendet weiterhin eine einzelne Zeichenfolge für das erste Argument, aber mit zwei Argumentnamen, die durch ein Komma getrennt sind, die zu Argumenten für die Testfunktion werden. In diesem Fall ist item und attribute.
Nachfolgend ist eine Liste von zwei Elementenpaaren. Jedes dieser Paare stellt ein item und ein attribute zu testendes Paar dar.
Wenn pytest keine Zeichenfolgendarstellung der übergebenen Objekte erstellen kann, wird eine erstellt. Sie können dies sehen, wenn Sie den Test ausführen:
$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 2 items
test_items.py::test_attributes[-format] PASSED [ 50%]
test_items.py::test_attributes[item1-append] PASSED [100%]
============================== 2 passed in 0.01s ===============================