Zum Inhalt

Beispielarbeiten · Code & Fehler

Fehlerbehebung mit Regressionstest: falsch gelesene Beträge

Ein Rechnungsprogramm las Beträge mit Tausenderpunkt falsch. Erst Tests, die den Fehler zeigen, dann die kleinste Änderung, dann alle Tests grün. Hier stehen Bericht und Änderung.

Musterarbeit (Demo), kein Kundenauftrag. Gebaut als Probeaufgabe mit erfundenen Beispieldaten; die Prüfzahlen unten stammen aus dem automatischen Prüflauf und sind nicht geschönt.

Was gebaut wurde

  • Zuerst Regressionstests, die vor der Reparatur rot sind
  • Dann nur die eine fehlerhafte Funktion geändert
  • Danach die bestehenden Tests und die neuen zusammen grün

fehlerbehebung-projekt.zip herunterladen

Wie geprüft wurde

Automatischer Prüflauf vom 18.10.2026, aus dem Protokoll (Testdaten erfunden, Zahlen gemessen):

  • 7/7 Schritte gruen
  • Kundensuite 9 Tests, 9 Regressionstests (9 rot vor Fix), nach Fix 18/18 gruen, geaendert: ['parse_betrag']
Alle Prüffälle (7 von 7 grün)
  • ✓ 1 Ausgangslage: Kundensuite gruen
  • ✓ 2 Reproduziert: alle Meldungsfaelle rot
  • ✓ 3 Reproduktion trifft nur neue Tests (alte gruen)
  • ✓ 4 Fix lokal: nur parse betrag geaendert
  • ✓ 5 Nach Fix alles gruen
  • ✓ 6 Dieselben Tests (alt + neu) gelaufen
  • ✓ 7 Gegenprobe: alter Code faellt durch die Regressionstests

Grenzen

Was diese Probe nicht abdeckt, steht hier, damit niemand mehr erwartet:

  • Probe deckt einen Python-Fehler mit vorhandener pytest-Suite ab; Fehler, die nur in Produktion (Last, Fremddienste, Hardware) auftreten, brauchen Logs/Zugang des Kunden
  • Fremdsprachen/Stacks ohne lokale Laufzeit (z. B. iOS, .NET-Desktop mit Lizenz) nicht abgedeckt
  • Fix wird nur mit Tests geliefert, nicht ungetestet direkt im Kundensystem eingespielt

Bericht

# Fehlerbehebung Rechnungsrechner

**Meldung:** '1.250 EUR' ergab Brutto 1,49 EUR statt 1.487,50 EUR; '1.234.567,89' liess das Programm abstuerzen.

**Ursache:** `parse_betrag` behandelte einen Punkt nur dann als Tausendertrenner, wenn zusaetzlich ein Komma da war -
'1.250' wurde als 1,25 gelesen; bei zwei Tausenderpunkten wurde nur der erste entfernt.

**Behebung:** Punkt ist Tausendertrenner, Komma Dezimaltrenner; ein Punkt ohne Dreiergruppen ist mehrdeutig und wird
abgewiesen (Fehlermeldung statt still falscher Rechnung). Nur `parse_betrag` geaendert - siehe `fix.diff`.

**Nachweis:** 9 Regressionstests, vor dem Fix 9 rot, danach alle
18 Tests gruen; gegen den alten Code wieder rot.

Die Änderung (Diff)

--- rechnung.py.orig
+++ rechnung.py
@@ -7,14 +7,30 @@
 CENT = Decimal("0.01")
 
 
+_TAUSENDER = re.compile(r"\d{1,3}(\.\d{3})+")
+
+
 def parse_betrag(text: str) -> Decimal:
-    """Betrag in deutscher Schreibweise lesen: '19,99', '1.234,50 EUR', '250'."""
+    """Betrag in deutscher Schreibweise lesen: '19,99', '1.234,50 EUR', '1.250 EUR', '250'.
+
+    Punkt ist in deutscher Schreibweise immer Tausendertrenner, Komma immer Dezimaltrenner. Ohne Komma ist ein
+    Punkt nur dann Tausendertrenner, wenn danach Dreiergruppen folgen ('1.250') - sonst ist die Eingabe mehrdeutig
+    und wird abgewiesen statt still falsch gerechnet.
+    """
     t = text.replace("EUR", "").replace("\u20ac", "").strip()
     if "," in t:
-        t = t.replace(",", ".")
-        if t.count(".") > 1:          # Tausenderpunkt
-            t = t.replace(".", "", 1)
-    return Decimal(t)
+        ganz, _, rest = t.partition(",")
+        if "." in rest or "," in rest or ("." in ganz and not _TAUSENDER.fullmatch(ganz)):
+            raise ValueError(f"Betrag nicht lesbar: {text!r}")
+        t = ganz.replace(".", "") + "." + rest
+    elif "." in t:
+        if not _TAUSENDER.fullmatch(t):
+            raise ValueError(f"Betrag mehrdeutig (Punkt ohne Dreiergruppen): {text!r}")
+        t = t.replace(".", "")
+    try:
+        return Decimal(t)
+    except InvalidOperation:
+        raise ValueError(f"Betrag nicht lesbar: {text!r}") from None
 
 
 def runden(betrag: Decimal) -> Decimal:

So etwas für deinen Betrieb?

Beschreib kurz, was du brauchst. Du bekommst einen Festpreis, bevor ich anfange; ohne dein Ja passiert nichts.

Zur Werkstatt