Abschnitt 19 · Lektion 5
Tests für das Werkzeug
Drei Teile des Werkzeugs stehen, und sie rechnen richtig. Heute jedenfalls. Nächsten Monat kommt ein neuer Laden dazu und mit ihm eine neue Regel. Im Frühjahr wechselt vielleicht die Bank, und irgendwann baut man am Einlesen etwas um, weil eine Spalte anders heißt. Jedes Mal soll der Rest weiter stimmen, und das von Hand nachzurechnen, macht niemand zweimal.
Wie Tests mit pytest gehen, steht in Abschnitt 15. Hier geht es darum, welche Tests ein Werkzeug wie dieses braucht und woher ihre Daten kommen.
Was man testet
Nicht alles ist gleich wichtig. Getestet wird zuerst, was sich ändern wird, und das, was schiefgeht, ohne dass man es sieht:
- Die Regeln. Sie wachsen jeden Monat, und eine Regel an der falschen Stelle sortiert still falsch ein. Ein Test je Eigenschaft: ein Treffer im zweiten Feld, Groß und klein, eine Buchung, auf die zwei Regeln passen, und eine, auf die keine passt.
- Das Einlesen fremder Dateien. Beide Kodierungen und eine Zeile, die kaputt ist. Hier kommen die Daten von außen, und von außen kommt irgendwann alles.
- Die Summen. Vor allem die Stelle, an der Einnahmen und Ausgaben getrennt werden, denn ein Fehler dort ergibt Zahlen, die plausibel aussehen.
Nicht getestet wird, wie die Tabelle von tabulate aussieht. Die zeichnet ein Paket, das seine
eigenen Tests hat. Ein Test, der jeden Strich der Tabelle vergleicht, wird außerdem rot, sobald eine
Kategorie einen längeren Namen bekommt, obwohl alles stimmt. Prüfen lohnt sich, was in der Tabelle
steht, und das geht mit in.
Kleine Daten im Test
import csv
def lies_regeln(pfad):
"""Liest Stichwort und Ordner aus einer CSV-Datei, in ihrer Reihenfolge."""
regeln = {}
with open(pfad, encoding="utf-8-sig", newline="") as datei:
for zeile in csv.DictReader(datei, delimiter=";"):
if zeile["Stichwort"]:
regeln[zeile["Stichwort"]] = zeile["Ordner"]
return regeln
def ordner_fuer(absender, betreff, regeln):
"""Gibt den Ordner für eine Mail zurück: die erste passende Regel, sonst Posteingang."""
text = (absender + " " + betreff).lower()
for stichwort, ordner in regeln.items():
if stichwort.lower() in text:
return ordner
return "Posteingang" # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) from postfach import ordner_fuer
REGELN = {"rechnung": "Rechnungen", "TSV": "Verein", "versandhaus": "Einkäufe"}
def test_stichwort_im_betreff():
assert ordner_fuer("service@versandhaus-elster.example", "Ihre Rechnung", REGELN) == "Rechnungen"
def test_gross_geschriebene_regel():
assert ordner_fuer("info@tsv-lindenau.example", "Einladung", REGELN) == "Verein"
def test_keine_regel_passt():
assert ordner_fuer("mia.berger@example.org", "Fotos vom Sommerfest", REGELN) == "Posteingang" Die Tests für die Ordnerregeln bringen ihre Regeln selbst mit, drei Stück in einem Dictionary. Die
Datei ordner.csv lesen sie nicht. Der Grund ist einfach. Die Datei soll sich ändern dürfen, ohne
dass ein Test rot wird, und in jedem Test sollen nur die Regeln stehen, auf die es bei ihm ankommt.
Wenn du später wissen willst, warum test_gross_geschriebene_regel rot ist, siehst du die Regel
TSV und die Mail gleich daneben. Nachsehen musst du in keiner Datei.
Jeder Test prüft eine Eigenschaft, und welche, steht schon in seinem Namen. Wird einer rot, steht der Name in der Zeile von pytest, und man weiß ohne Lesen, was kaputt ist.
Ein Ordner nur für den Test
from pathlib import Path
def lies_text(pfad):
"""Liest eine Textdatei, erst als UTF-8 und sonst als cp1252."""
try:
return Path(pfad).read_text(encoding="utf-8-sig"), "utf-8"
except UnicodeDecodeError:
return Path(pfad).read_text(encoding="cp1252"), "cp1252" # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) from lesen import lies_text
def test_utf8(tmp_path):
datei = tmp_path / "liste.csv"
datei.write_text("Nachname;Vorname\nKrüger;Tim\n", encoding="utf-8")
assert lies_text(datei) == ("Nachname;Vorname\nKrüger;Tim\n", "utf-8")
def test_cp1252(tmp_path):
datei = tmp_path / "liste.csv"
datei.write_text("Nachname;Vorname\nKrüger;Tim\n", encoding="cp1252")
assert lies_text(datei) == ("Nachname;Vorname\nKrüger;Tim\n", "cp1252") Fürs Einlesen braucht ein Test eine Datei. Die soll er sich selbst schreiben, damit es egal ist, was gerade im Ordner liegt. Dafür gibt es tmp_path.
Steht dieses Wort als Parameter in der Testfunktion, bekommt sie von pytest einen eigenen, leeren
Ordner als Path, für jeden Test einen neuen. Der Test schreibt dort seine Datei, mit genau der
Kodierung, um die es geht, und ruft danach die Funktion damit auf.
Woher der Ordner kommt, muss man nicht wissen. Er liegt bei den vorübergehenden Dateien des Systems,
unter Linux und hier in /tmp, und alte Ordner räumt pytest von selbst weg.
So ein Parameter, den pytest von selbst füllt, heißt Fixture. tmp_path ist die, die man am
häufigsten braucht, und für dieses Werkzeug die einzige.
Ein Test, der nächsten Monat rot wird
Mitgliederliste TSV Lindenau
Stand: 01.10.2026
Nachname;Vorname;Telefon;Status
Zimmermann;Ole;0176 5554443;aktiv
Krüger;Tim;0160 1112223;passiv
Schäfer;Paul;06221 44556;aktiv
Müller;Jürgen;06221 33221;aktiv
Lorenz;Mia;0170 9998887;passiv from pathlib import Path
def lies_text(pfad):
"""Liest eine Textdatei, erst als UTF-8 und sonst als cp1252."""
try:
return Path(pfad).read_text(encoding="utf-8-sig"), "utf-8"
except UnicodeDecodeError:
return Path(pfad).read_text(encoding="cp1252"), "cp1252" # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) from lesen import lies_text
def test_die_echte_liste():
text, kodierung = lies_text("mitglieder.csv")
zeilen = text.splitlines()
assert len(zeilen) == 8 Dieser Fehler ist bequem. Der Test liest die echte mitglieder.csv und prüft, ob sie acht Zeilen
hat. Als er geschrieben wurde, stimmte das. Dann ist Mia in den Verein eingetreten, und der Test ist
rot: assert 9 == 8. An lies_text hat sich nichts geändert. Der Test meldet einen Fehler, den es
gar nicht gibt.
Mit dem Kontoauszug passiert das jeden Monat, weil jeder Monat eine neue Datei bringt. Ein Test, der mit dem echten Export rechnet, ist am Ersten rot, und nach dem dritten Mal nimmt ihn niemand mehr ernst.
Die Daten eines Tests kommen deshalb aus dem Test selbst. Ob das Werkzeug mit dem echten Export klarkommt, merkst du beim Benutzen.
Ein Fehler zur Probe
import csv
def lies_regeln(pfad):
"""Liest Stichwort und Ordner aus einer CSV-Datei, in ihrer Reihenfolge."""
regeln = {}
with open(pfad, encoding="utf-8-sig", newline="") as datei:
for zeile in csv.DictReader(datei, delimiter=";"):
if zeile["Stichwort"]:
regeln[zeile["Stichwort"]] = zeile["Ordner"]
return regeln
def ordner_fuer(absender, betreff, regeln):
"""Gibt den Ordner für eine Mail zurück: erst im Absender gesucht, dann im Betreff."""
for feld in [absender, betreff]:
for stichwort, ordner in regeln.items():
if stichwort.lower() in feld.lower():
return ordner
return "Posteingang" # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) from postfach import ordner_fuer
REGELN = {"rechnung": "Rechnungen", "TSV": "Verein", "versandhaus": "Einkäufe"}
def test_stichwort_im_betreff():
assert ordner_fuer("service@versandhaus-elster.example", "Ihre Rechnung", REGELN) == "Rechnungen"
def test_gross_geschriebene_regel():
assert ordner_fuer("info@tsv-lindenau.example", "Einladung", REGELN) == "Verein"
def test_keine_regel_passt():
assert ordner_fuer("mia.berger@example.org", "Fotos vom Sommerfest", REGELN) == "Posteingang" Ob ein Test etwas bringt, sieht man wie in Abschnitt 15 erst, wenn man einen Fehler einbaut. Hier
steht in postfach.py die Fassung aus Lektion 19.3, in der erst die Felder und dann die Regeln
durchgegangen werden. Der Test mit der Rechnung vom Versandhaus wird rot, und in seiner Meldung
steht, welcher Ordner herauskam.
Ohne diesen Test wäre die Umstellung durchgegangen. Die Rechnung läge seitdem bei den Einkäufen.
Was die Aufgabe will
In test_auswerter.py kommen Tests für das ganze Werkzeug, mindestens sechs, für das Einlesen, für
die Regeln und für die Übersicht. Beim Prüfen werden danach Fehler in die drei Teile eingebaut, und
jeder muss einem deiner Tests auffallen.
Zum Mitnehmen
Zuerst bekommen die Teile Tests, die sich ändern werden, also die Regeln, das Lesen fremder Dateien und die Summen. Jeder Test bringt seine eigenen kleinen Daten mit, in denen genau sein Fall steckt, und liest nicht die echte Datei, die im nächsten Monat schon anders aussieht. Braucht ein Test eine Datei, legt er sie in tmp_path an, einem Ordner, den pytest für jeden Test frisch bereitstellt.
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.