Abschnitt 15 · Lektion 4
Fehler erwarten
Über 31500 Gramm gibt porto() keinen Preis zurück. Stattdessen kommt ein ValueError mit der
Meldung Zu schwer, das geht nur als Spedition, so wie in Abschnitt 9. Für diesen vierten Fall gibt
es bisher keinen Test.
Ein Test dafür dreht die gewohnte Regel um. Sonst macht ein Fehler in einer Testfunktion den Test rot. Hier soll er ihn grün machen, aber nur, wenn er auch wirklich kommt.
Ein Fehler, der kommen soll
def porto(gramm):
"""Was ein Paket kostet, nach seinem Gewicht in Gramm."""
if gramm <= 2000:
return 3.99
elif gramm <= 10000:
return 5.99
elif gramm <= 31500:
return 9.99
else:
raise ValueError("Zu schwer, das geht nur als Spedition") # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) import pytest
from porto import porto
def test_schwerstes_paket():
assert porto(31500) == 9.99
def test_zu_schwer():
with pytest.raises(ValueError, match="Spedition"):
porto(31501) Dafür gibt es in pytest pytest.raises. Es steht hinter einem with, das du vom Öffnen einer Datei
kennst. Darunter kommt eingerückt der Aufruf, der den Fehler auslösen soll. Endet der Block mit
einem ValueError, fängt pytest.raises ihn ab, und der Test ist grün.
Die beiden Tests sind das Grenzpaar aus der letzten Lektion, diesmal für die letzte Grenze. 31500 Gramm sind das schwerste Paket, das noch etwas kostet, 31501 Gramm das erste, das abgelehnt wird.
Mit match="Spedition" wird zusätzlich die Meldung geprüft. Der Text muss irgendwo darin vorkommen.
Genau genommen ist er ein Suchmuster, in dem ein paar Zeichen eine eigene Bedeutung haben, ein Punkt
etwa passt dort auf jedes beliebige Zeichen. Für Meldungen wie diese ist das egal.
Der Fehler bleibt aus
def porto(gramm):
"""Was ein Paket kostet, nach seinem Gewicht in Gramm."""
if gramm <= 2000:
return 3.99
elif gramm <= 10000:
return 5.99
else:
return 9.99 # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) import pytest
from porto import porto
def test_schwerstes_paket():
assert porto(31500) == 9.99
def test_zu_schwer():
with pytest.raises(ValueError, match="Spedition"):
porto(31501) Jemand hat porto() vereinfacht. Alles über 10 kg kostet jetzt 9,99 Euro, und die Ablehnung ist
weg. Der Test für 31500 Gramm bleibt grün, die kosten ja wirklich 9,99 Euro. Rot wird nur der Test
für 31501 Gramm, mit Failed: DID NOT RAISE ValueError. Der Fehler ist also nicht gekommen.
Ohne diesen Test würde der Shop jetzt ein Paket mit 40 kg für 9,99 Euro annehmen, und niemand würde es merken. Deshalb ist ein Test für den Fehlerfall mindestens so viel wert wie einer für den Normalfall. Wenn eine Ablehnung wegfällt, macht das keinen Lärm. Das Programm tut danach einfach etwas, das es nicht tun sollte, und meldet dabei keinen Fehler.
Die falsche Meldung
def porto(gramm):
"""Was ein Paket kostet, nach seinem Gewicht in Gramm."""
if gramm <= 2000:
return 3.99
elif gramm <= 10000:
return 5.99
elif gramm <= 31500:
return 9.99
else:
raise ValueError("Zu schwer für ein Paket") # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) import pytest
from porto import porto
def test_schwerstes_paket():
assert porto(31500) == 9.99
def test_zu_schwer():
with pytest.raises(ValueError, match="Spedition"):
porto(31501) Hier lehnt porto() noch ab, mit dem richtigen ValueError, aber mit einer neuen Meldung. Von der
Spedition ist darin keine Rede mehr. Rot wird der Test trotzdem, wegen match.
pytest schreibt Regex pattern did not match., das Suchmuster hat also nicht gepasst. Regex ist die
Kurzform von regulärer Ausdruck, so heißen solche Muster. Darunter stehen das Muster und die
Meldung, die wirklich kam, Expected regex: 'Spedition' und
Actual message: 'Zu schwer für ein Paket'.
Ob die Meldung wichtig genug für einen Test ist, hängt davon ab, wer sie liest. Wenn die Kundin sie im Shop zu sehen bekommt, gehört der Hinweis auf die Spedition zum Ergebnis wie der Preis. Dann gehört er auch in den Test.
Irgendein Fehler
def porto(gramm):
"""Was ein Paket kostet, nach seinem Gewicht in Gramm."""
if gramm <= 2000:
return 3.99
elif gramm <= 10000:
return 5.99
elif gramm <= 31500:
return 9.99
else:
raise ValueEror("Zu schwer, das geht nur als Spedition") # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) import pytest
from porto import porto
def test_zu_schwer_irgendein_fehler():
with pytest.raises(Exception):
porto(31501)
def test_zu_schwer_valueerror():
with pytest.raises(ValueError):
porto(31501) Der Fehler dieser Lektion ist ein Tippfehler in porto(). Da steht ValueEror statt ValueError,
und deshalb löst Python einen NameError aus statt der Ablehnung.
Der erste Test erwartet nur Exception, und der ist grün. Exception ist der Oberbegriff für fast
alle Fehlerarten, auch für NameError. Das kennst du aus Lektion 9.2 von except Exception. Der
Test ist also mit jedem Fehler zufrieden, auch mit diesem.
Der zweite Test erwartet einen ValueError und wird rot. pytest zeigt dabei den ganzen Weg, erst
die Zeile im Test, dann die Zeile in porto.py und darunter
NameError: name 'ValueEror' is not defined. Wenn ein Test genau die Fehlerart erwartet, die die
Funktion verspricht, heißt sein Grün auch, dass die Funktion sich daran hält.
Was die Aufgabe will
ueberweise() aus Lektion 9.4 lehnt zwei Dinge mit einem ValueError ab. Einen Betrag, der nicht
größer als null ist, und einen, der größer ist als der Kontostand. Du schreibst Tests für eine
Überweisung, die klappt, und für beide Ablehnungen samt Meldung. Danach baut pruefen.py Fehler in
die Funktion ein, auch solche, die nur ein Test mit match bemerkt.
Zum Mitnehmen
Mit with pytest.raises(ValueError) erwartet ein Test, dass der Code darunter genau diese Fehlerart auslöst, und er wird rot, wenn sie ausbleibt. Mit match= kommt dazu, dass ein bestimmter Text in der Meldung stehen muss. Ein Test, der nur Exception erwartet, nimmt jeden Fehler hin, auch einen Tippfehler.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.