Python-Kurs / Mit unittest Verhalten und Fehlerfälle automatisch prüfen

Automatische Tests prüfen wiederholbar, ob festgelegte Eingaben zu den erwarteten Ergebnissen oder Ausnahmen führen. Formulieren Sie dabei das benötigte Verhalten einschließlich Grenzen und Fehlerfällen, damit spätere Änderungen eine Regression sichtbar machen können.

Den Vertrag vor dem Test festlegen

Die folgende Summenfunktion erwartet eine Liste nichtnegativer Ganzzahlen. Eine leere Liste ergibt null; negative Werte, boolesche Werte und Zahlen als Text sollen abgelehnt werden. Dieser Vertrag ist enger als „alles, was sich irgendwie addieren lässt“. Die Tests sollen genau diese fachliche Auswahl schützen.

Für den Einstieg eignet sich eine reine Berechnung ohne Uhrzeit, Netzwerk oder Benutzerdateien. Ihr Ergebnis lässt sich reproduzieren, ohne eine bestimmte externe Umgebung vorauszusetzen. unittest gehört zur Python-Standardbibliothek. Dateien mit dem Präfix test_ und Testmethoden mit test_ werden im gezeigten Aufbau durch die automatische Suche gefunden.

Ein Test vergleicht nicht bloß gedruckte Texte, die ein Mensch danach beurteilt. Ein fehlgeschlagener Vergleich wird vom Testläufer erfasst und beeinflusst dessen Ergebnis. Die Unittest-Referenz beschreibt Testfälle, Vergleiche und Ausnahmeprüfungen.

Funktion und Tests als vollständige Dateien speichern

Erstellen Sie einen neuen Übungsordner und speichern Sie darin zunächst expense_math.py mit UTF-8:

def total(amounts):
    if any(type(x) is not int or x < 0 for x in amounts):
        raise ValueError("invalid amount")
    return sum(amounts)

Speichern Sie daneben die vollständige Datei test_expense_math.py:

import unittest
from expense_math import total

class TotalTests(unittest.TestCase):
    def test_regular(self):
        self.assertEqual(total([3500, 8000, 2500]), 14000)

    def test_empty(self):
        self.assertEqual(total([]), 0)

    def test_invalid(self):
        for values in [[-1], [True], ["8000"]]:
            with self.subTest(values=values):
                with self.assertRaises(ValueError):
                    total(values)

if __name__ == "__main__":
    unittest.main()

Führen Sie in diesem Ordner mit der ausgewählten Projektumgebung aus:

python -m unittest -v

Erwartet werden drei Testmethoden und am Ende OK. Die Laufzeit und genaue Textanordnung hängen von der Umgebung ab. Unter macOS oder Ubuntu kann der Interpreterbefehl außerhalb einer virtuellen Umgebung python3 heißen. Wenn null Tests gefunden werden, prüfen Sie Arbeitsordner, Dateinamen und Importierbarkeit, statt das bloße Fehlen einer Fehlermeldung als vollständigen Erfolg zu werten.

assertEqual vergleicht erwartete und tatsächliche Werte. assertRaises(ValueError) erwartet genau den vorgesehenen Ausnahmebereich; bleibt die Ausnahme aus, scheitert der Test. subTest unterscheidet die drei Eingaben innerhalb derselben Methode. Dadurch erhalten Sie weiterhin drei Testmethoden, obwohl die Methode für ungültige Werte mehrere Fälle untersucht.

Die Funktion durchläuft die Liste bei der Validierung und anschließend beim Summieren. Der Vertrag gilt hier für wiederholt durchlaufbare Listen. Ein einmaliger Iterator könnte durch die erste Prüfung bereits verbraucht sein; die gezeigten Tests belegen deshalb nicht, dass beliebige Iterable als Eingabe unterstützt werden.

Bewusst einen Fehler einbauen und die Schutzwirkung prüfen

  1. Ändern Sie nur den Rückgabewert vorübergehend in sum(amounts) + 1. Starten Sie die Tests erneut. Sowohl der normale Fall als auch die leere Liste müssen scheitern, weil die Summe um eins zu hoch ist.
  2. Stellen Sie return sum(amounts) wieder her. Alle drei Methoden müssen erneut erfolgreich sein.
  3. Entfernen Sie nur vorübergehend den Validierungsblock. Der negative Wert und True werden dann vom normalen Summieren nicht mit dem verlangten ValueError zurückgewiesen. Für die Zeichenfolge kann eine andere Ausnahme auftreten. Die Testausgabe muss diese Abweichungen sichtbar machen.
  4. Stellen Sie die ursprüngliche Funktion vollständig wieder her und prüfen Sie abschließend erneut OK. Lassen Sie die absichtlich fehlerhafte Variante nicht als endgültige Implementierung zurück.

Diese kontrollierte Änderung prüft, ob die Tests die beabsichtigte Regel tatsächlich schützen. Eine erfolgreiche Ausführung zeigt dagegen immer nur die geprüften Fälle. Ergänzen Sie bei einem erweiterten Vertrag gezielt neue Grenzen, statt für jede Implementierungszeile einen nachgebauten Test zu schreiben.

Dateien und Prozessstatus durch einen Integrationstest verbinden

Einheitstests der Funktion belegen weder korrekte CSV-Spalten noch einen richtigen Dateipfad oder Datenbankanschluss. Ein Integrationstest verbindet mehrere Teile und prüft ihre gemeinsame Wirkung. Verwenden Sie dafür unabhängige Testdaten und einen eigenen temporären Ordner, damit bestehende Berichte nicht überschrieben werden.

Die folgende zusätzliche Übung benötigt die vollständige Datei csv_report.py aus dem CSV-Berichtsprojekt. Legen Sie dessen unverändertes vollständiges Programm neben die hier gezeigte Datei. Es liest expenses-input.csv, schreibt report.csv und rejected.csv und liefert bei verworfenen Datensätzen den Status eins. Die Abhängigkeit ist notwendig; check_csv.py enthält nicht selbst das Berichtsprogramm.

Speichern Sie nun die folgende vollständige Datei check_csv.py und führen Sie im selben Übungsordner python check_csv.py aus:

import subprocess
import sys
import tempfile
from pathlib import Path
program = Path("csv_report.py").resolve()
with tempfile.TemporaryDirectory() as folder:
    work = Path(folder)
    source = work / "expenses-input.csv"
    source.write_text("id,category,amount\ne1,food,8000\ne1,food,8000\n", encoding="utf-8")
    result = subprocess.run([sys.executable, str(program)], cwd=work, capture_output=True, text=True)
    assert result.returncode == 1
    assert "food,8000" in (work / "report.csv").read_text(encoding="utf-8")
    assert "duplicate id" in (work / "rejected.csv").read_text(encoding="utf-8")
print("CSV integration checks passed")

Erwartete Ausgabe:

CSV integration checks passed

Der absolute Pfad des Berichtsprogramms wird vor dem Verzeichniswechsel ermittelt. Der Unterprozess arbeitet danach im temporären Ordner und verwendet denselben Interpreter wie der Test. Die erste Zeile mit der Kennung e1 wird angenommen, die zweite als Duplikat verworfen. Der Gesamtbetrag der angenommenen Zeile muss 8000 sein. Der Test verlangt deshalb den Fehlerstatus eins, einen passenden Bericht und den Ablehnungsgrund duplicate id.

Ein von null verschiedener Prozessstatus ist in diesem Integrationstest gerade das erwartete Verhalten des getesteten Programms, weil die Eingabe absichtlich einen Fehler enthält. Der Test selbst endet trotzdem erfolgreich, wenn genau dieser Vertrag erfüllt ist. Nach dem Block bereinigt TemporaryDirectory seinen eigenen Arbeitsordner; die ursprünglichen Berichte des Projekts werden nicht verwendet. Details zur Lebensdauer erläutert die Tempfile-Dokumentation.

Dieser kleine Prüfhelfer verwendet normale assert-Anweisungen. Starten Sie ihn ohne -O oder PYTHONOPTIMIZE, da diese Prüfungen sonst entfallen können. Die Assertions von unittest.TestCase im ersten Beispiel sind Methodenaufrufe und davon zu unterscheiden. Für dauerhafte Prüfprogramme können Sie die Integration ebenfalls als TestCase mit entsprechenden Vergleichsmethoden formulieren.

Halten Sie bei der Ergebnisbewertung Interpreter-Version, Befehl, erwartete und tatsächliche Ausgabe sowie den geprüften Umfang auseinander. Erfolgreiche Funktions- und CSV-Tests sind keine Bestätigung ungeprüfter Installationsbildschirme, Betriebssysteme oder Datenbankkonfigurationen.