Abschnitt 15 · Lektion 5
Erst der Test, dann der Code
Eine Funktion für Rechnungsnummern soll RE-2026-0042 liefern. Es gibt sie noch nicht, aber drei
Tests stehen schon da, und in denen steht, was sie können muss. Bis jetzt kam der Test immer nach
dem Code. Die Funktion stand da, und die Tests hielten fest, was sie tut. Hier geht es andersherum.
Das klingt umständlich, aber der Vorteil zeigt sich schnell, weil du dich entscheiden musst, was
herauskommen soll, bevor du darüber nachdenkst, wie.
Rot
def rechnungsnummer(jahr, nummer):
"""Gibt die Rechnungsnummer zurück, etwa RE-2026-0042.""" # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) import pytest
from rechnung import rechnungsnummer
def test_vierstellig_mit_nullen():
assert rechnungsnummer(2026, 42) == "RE-2026-0042"
def test_lange_nummern_bleiben_ganz():
assert rechnungsnummer(2026, 12345) == "RE-2026-12345"
def test_null_gibt_es_nicht():
with pytest.raises(ValueError, match="fangen bei 1 an"):
rechnungsnummer(2026, 0) Gebraucht wird eine Funktion für Rechnungsnummern wie RE-2026-0042. Erst das Jahr, dann die
laufende Nummer mit mindestens vier Stellen, vorn mit Nullen aufgefüllt. Längere Nummern bleiben
ganz, und gezählt wird ab 1. Jeder der drei Tests prüft einen Teil davon.
In rechnung.py steht dagegen nur die erste Zeile der Funktion samt Docstring. Das ist gültiges
Python. Die Funktion tut nur nichts und gibt None zurück.
Also sind alle drei rot. Beim ersten steht assert None == 'RE-2026-0042', beim zweiten dasselbe
mit 'RE-2026-12345' und beim dritten DID NOT RAISE ValueError. Das ist der erste Schritt, und er
gehört dazu. Ein Test, der grün ist, bevor es den Code gibt, prüft nichts, das kennst du aus den
letzten Lektionen.
Rot sein muss er außerdem aus dem richtigen Grund. Fehlt die Funktion in rechnung.py ganz, kommt
pytest gar nicht erst zu den Tests und meldet beim Einsammeln
ImportError: cannot import name 'rechnungsnummer'.
Grün
def rechnungsnummer(jahr, nummer):
"""Gibt die Rechnungsnummer zurück, etwa RE-2026-0042."""
if nummer < 1:
raise ValueError("Rechnungsnummern fangen bei 1 an")
text = str(nummer)
while len(text) < 4:
text = "0" + text
return "RE-" + str(jahr) + "-" + text # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) import pytest
from rechnung import rechnungsnummer
def test_vierstellig_mit_nullen():
assert rechnungsnummer(2026, 42) == "RE-2026-0042"
def test_lange_nummern_bleiben_ganz():
assert rechnungsnummer(2026, 12345) == "RE-2026-12345"
def test_null_gibt_es_nicht():
with pytest.raises(ValueError, match="fangen bei 1 an"):
rechnungsnummer(2026, 0) Die Funktion bekommt gerade so viel Code, dass die Tests grün werden. Erst die Ablehnung für Nummern
unter 1, dann die Nummer als Text, und vorn kommt so lange eine Null davor, bis sie vier Zeichen
hat. Schön ist das nicht. Aber 3 passed.
„Gerade genug“ heißt auch, dass die Funktion noch nicht kann, was kein Test verlangt. Soll sie später etwa ein Jahr vor 2000 ablehnen, kommt zuerst ein Test dafür und erst dann der Code.
Aufräumen
def rechnungsnummer(jahr, nummer):
"""Gibt die Rechnungsnummer zurück, etwa RE-2026-0042."""
if nummer < 1:
raise ValueError("Rechnungsnummern fangen bei 1 an")
return f"RE-{jahr}-{nummer:4d}" # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) import pytest
from rechnung import rechnungsnummer
def test_vierstellig_mit_nullen():
assert rechnungsnummer(2026, 42) == "RE-2026-0042"
def test_lange_nummern_bleiben_ganz():
assert rechnungsnummer(2026, 12345) == "RE-2026-12345"
def test_null_gibt_es_nicht():
with pytest.raises(ValueError, match="fangen bei 1 an"):
rechnungsnummer(2026, 0) Die Schleife lässt sich durch einen f-String ersetzen, und genau dafür sind die Tests da. Der erste
Versuch, {nummer:4d}, macht einen davon sofort rot.
Bei Text zeigt pytest, wo sich die beiden Seiten unterscheiden. In der Zeile mit dem Minus steht,
was erwartet war, RE-2026-0042. In der mit dem Plus steht, was herauskam, RE-2026- 42, und die
Dächer darunter zeigen auf die Stelle. 4d füllt mit Leerzeichen auf, nicht mit Nullen.
def rechnungsnummer(jahr, nummer):
"""Gibt die Rechnungsnummer zurück, etwa RE-2026-0042."""
if nummer < 1:
raise ValueError("Rechnungsnummern fangen bei 1 an")
return f"RE-{jahr}-{nummer:04d}" # Startet pytest, wie python -m pytest -q es im Terminal tut.
import pytest
pytest.main(["-q"]) import pytest
from rechnung import rechnungsnummer
def test_vierstellig_mit_nullen():
assert rechnungsnummer(2026, 42) == "RE-2026-0042"
def test_lange_nummern_bleiben_ganz():
assert rechnungsnummer(2026, 12345) == "RE-2026-12345"
def test_null_gibt_es_nicht():
with pytest.raises(ValueError, match="fangen bei 1 an"):
rechnungsnummer(2026, 0) Mit {nummer:04d} sind wieder alle drei grün. Die 0 vor der 4 füllt vorn mit Nullen auf, auf
mindestens vier Stellen, und was länger ist, bleibt ganz. Zwischen den beiden Versuchen lag ein
einziger Lauf von pytest, und der hat den Fehler gefunden, bevor ihn jemand auf einer Rechnung
gesehen hat. Das ist mit „aufräumen mit Netz“ gemeint. Du kannst den Code ohne Angst ändern, weil du
nach jedem Schritt siehst, ob er noch tut, was er soll.
Der Fehler, der in diesem Schritt am nächsten liegt, ist kein Fehler in Python. Ein Test ist rot,
und statt den Code zu reparieren, ändert man den Test, bis er zum Code passt, hier also auf
RE-2026- 42. Dann ist alles grün. Nur beschreiben die Tests jetzt nicht mehr, was die Funktion
tun soll, sondern bloß noch, was sie gerade tut.
Wann sich das lohnt
Dieser Kreis heißt rot, grün, aufräumen, und das ganze Vorgehen testgetriebene Entwicklung (test-driven development, kurz TDD). Wenn du den Kreis eine Weile gefahren bist, merkst du, dass die Tests dabei mehr sind als eine Kontrolle. Sie sind die genaueste Beschreibung der Funktion, die es gibt. Nötig ist das nicht bei jeder Funktion. Bei allem, was mit Geld oder mit Daten anderer Leute zu tun hat, lohnt es sich.
Was die Aufgabe will
In test_iban.py stehen fünf fertige Tests für eine Funktion iban_formatieren(), die eine IBAN in
Vierergruppen schreibt. Von der Funktion steht in iban.py nur die erste Zeile samt Docstring. Den
Rest schreibst du, bis alle fünf grün sind. Danach ruft pruefen.py sie noch mit IBANs auf, die in
keinem der Tests stehen. Fünf Tests sind eben nie alles.
Zum Mitnehmen
Beim Kreis aus Rot, Grün und Aufräumen stehen zuerst die Tests für das da, was eine Funktion können soll, und alle sind rot. Danach reicht knapp genug Code, um sie grün zu machen. Beim Aufräumen zeigen dieselben Tests nach jedem Schritt, ob noch alles stimmt.
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.