Exceptions und Tests: Kurzeinführung
Diese Kurzeinführung erläutert die Grundlagen von Exception Handling und Testing in Python und dient zur Unterstützung bei der Bearbeitung der Übungsaufgaben. Ein entsprechendes Short-Video wird noch bereitgestellt.
Error Handling mit Exceptions
Motivation
Bei der Entwicklung von Programmen müssen Fehler systematisch erkannt und behandelt werden. Der naive Ansatz, Fehler über Rückgabewerte zu signalisieren, stößt schnell an Grenzen:
# Problematischer Ansatz mit Rückgabewerten
def konto_abheben(kontostand, betrag):
if betrag > kontostand:
return -1 # Fehlerwert
return kontostand - betrag
# Problem: Wie unterscheiden wir echte Kontostände von Fehlercode?
neuer_stand = konto_abheben(100, 200)
if neuer_stand == -1:
print("Nicht genug Guthaben")
# Was wenn -1 ein gültiger Kontostand sein könnte?
Python bietet mit Exceptions einen eleganten Mechanismus zur Fehlerbehandlung:
def konto_abheben(kontostand, betrag):
if betrag > kontostand:
raise ValueError("Nicht genug Guthaben")
return kontostand - betrag
# Klare Fehlerbehandlung mit try-except
try:
neuer_stand = konto_abheben(100, 200)
except ValueError as e:
print(f"Fehler: {e}")
Grundlagen des Exception Handling
Eine Exception unterbricht den normalen Programmfluss und kann an beliebiger Stelle “aufgefangen” werden:
def validate_betrag(betrag):
if betrag <= 0:
raise ValueError("Betrag muss positiv sein")
return betrag
def einzahlung_durchfuehren():
betrag = validate_betrag(-50) # Exception wird hier ausgelöst
print("Wird nie erreicht")
try:
einzahlung_durchfuehren() # Exception wird hier gefangen
except ValueError as e:
print(f"Fehler bei Einzahlung: {e}")
print("Programm läuft weiter")
Testing mit assert und pytest
Von einfachen Tests zu systematischem Testing
Fehler in Programmen zu finden ist einfacher, wenn wir systematisch testen. Ein naiver Ansatz wäre:
# Naive Testmethode mit if-Statements
def konto_anlegen(start_guthaben):
if start_guthaben < 0:
raise ValueError("Startguthaben muss positiv sein")
return {"guthaben": start_guthaben}
# Umständlicher Test
if konto_anlegen(100)["guthaben"] != 100:
print("Fehler: Konto wurde nicht korrekt angelegt")
try:
konto_anlegen(-50)
print("Fehler: Negatives Guthaben wurde akzeptiert")
except ValueError:
print("OK: Negatives Guthaben wurde abgelehnt")
Python bietet mit assert
eine prägnantere Syntax für Tests:
# Bessere Tests mit assert
def test_konto_manuell():
# Test für positives Guthaben
konto = konto_anlegen(100)
assert konto["guthaben"] == 100, "Startguthaben falsch"
# Test für negatives Guthaben
try:
konto_anlegen(-50)
assert False, "Negatives Guthaben wurde akzeptiert"
except ValueError:
pass # Test erfolgreich
Strukturierte Tests mit pytest
pytest vereinfacht das Testen erheblich. Erstellen Sie zwei Dateien im gleichen Verzeichnis. Erstens die Datei mit dem Code, den wir testen wollen:
# bank.py
class Konto:
def __init__(self, start_guthaben):
if start_guthaben < 0:
raise ValueError("Startguthaben muss positiv sein")
self.guthaben = start_guthaben
def einzahlen(self, betrag):
if betrag <= 0:
raise ValueError("Einzahlungsbetrag muss positiv sein")
self.guthaben += betrag
Zweitens die Datei mit den Tests:
# test_bank.py
from bank import Konto
import pytest
def test_konto_anlegen():
konto = Konto(100)
assert konto.guthaben == 100
def test_konto_negativ():
# Die with-Syntax ist eine kompakte Schreibweise zur Prüfung,
# ob eine bestimmte Exception ausgelöst wird.
# Dies ist äquivalent zu:
# try:
# Konto(-50)
# assert False, "ValueError wurde erwartet"
# except ValueError:
# pass
with pytest.raises(ValueError):
Konto(-50)
def test_einzahlung():
konto = Konto(100)
konto.einzahlen(50)
assert konto.guthaben == 150
Die with pytest.raises()
Syntax ist eine von pytest bereitgestellte Möglichkeit zu prüfen, ob ein bestimmter Codeblock die erwartete Exception auslöst. Sie ist kürzer und klarer als die entsprechende try-except Konstruktion. Obwohl die with-Syntax für Sie neu ist, ist sie in Python-Tests sehr gebräuchlich und wird Ihnen häufig begegnen.
Die Tests werden dann mit pytest
wird über die Kommandozeile ausgeführt:
pytest test_bank.py
Dabei gilt:
- Testfunktionen müssen mit
test_
beginnen - Jeder Test sollte einen spezifischen Aspekt prüfen
- Tests sollten unabhängig voneinander sein
- pytest findet und führt die Tests automatisch aus
Best Practices für Tests
Testfälle strukturieren
Tests sollten übersichtlich und wartbar sein. Jeder Test sollte einen klar definierten Aspekt prüfen:
# Gute Strukturierung:
def test_konto_anlegen_standard():
konto = Konto(100)
assert konto.guthaben == 100
def test_konto_einzahlung_positiv():
konto = Konto(100)
konto.einzahlen(50)
assert konto.guthaben == 150
def test_konto_einzahlung_negativ():
konto = Konto(100)
with pytest.raises(ValueError):
konto.einzahlen(-50)
# Weniger gut - zu viele Aspekte in einem Test:
def test_konto_alles():
# Zu viele verschiedene Tests in einer Funktion
konto = Konto(100)
assert konto.guthaben == 100
konto.einzahlen(50)
assert konto.guthaben == 150
with pytest.raises(ValueError):
konto.einzahlen(-50)
Grenzfälle testen
Besonders wichtig ist das Testen von Grenzfällen:
- Standardfälle (normale Werte)
- Grenzwerte (0, Maximalwerte)
- Fehlerfälle (ungültige Eingaben)
def test_konto_grenzfaelle():
# Minimaler erlaubter Wert
konto = Konto(0)
assert konto.guthaben == 0
# Sehr große Werte
konto_gross = Konto(1000000)
assert konto_gross.guthaben == 1000000
# Einzahlung von 0
with pytest.raises(ValueError):
konto.einzahlen(0)
pytest Ausführung
Die wichtigsten pytest Kommandos:
# Alle Tests im aktuellen Verzeichnis
pytest
# Bestimmte Testdatei
pytest test_bank.py
# Ausführliche Ausgabe
pytest -v
# Bei erstem Fehler stoppen
pytest -x
pytest zeigt bei Fehlern hilfreiche Details:
===== FAILURES =====
def test_konto_einzahlung_positiv():
konto = Konto(100)
konto.einzahlen(50)
> assert konto.guthaben == 150
E assert 149 == 150
Debugging in VS Code
Die CS50-Entwicklungsumgebung unterstützt beim systematischen Finden von Fehlern (Debugging). Wie bereits aus C bekannt, können Sie dafür den integrierten Debugger nutzen:
debug50 python myscript.py
Der Debugger ermöglicht:
- Setzen von Breakpoints durch Klick auf die Zeilennummer
- Schrittweises Durchlaufen des Codes
- Inspektion von Variablenwerten
- Beobachten des Call Stacks
# Beispiel für Code, den wir debuggen möchten
def process_konto(start_guthaben, einzahlungen):
konto = Konto(start_guthaben) # Breakpoint hier setzen
for betrag in einzahlungen:
konto.einzahlen(betrag) # Werte schrittweise prüfen
return konto.guthaben
# Bei diesem Aufruf können wir die Werte beobachten
result = process_konto(100, [50, -30, 20])
Systematisches Debuggen hilft dabei:
- Den Programmablauf zu verstehen
- Fehlerhafte Zustände zu erkennen
- Die Ursache von Exceptions zu finden
Details zur Nutzung des Debuggers haben wir uns bereits bei C angeschaut.