====== LU07e - Sicher refactoren ====== Refactoring ohne Sicherheitsnetz ist Raten. Diese Seite zeigt, wie Sie belegen, dass das Verhalten gleich geblieben ist - und welche Umbauten es still und leise doch verändern. Für das [[modul:m323:leistungsbeurteilungen:lb02:start|Portfolio]] verlangt **D1I** genau das: Vorher/Nachher aus Ihrer Commit-Historie, //gleiches Verhalten per Test belegt//. Ohne Test ist der Nachweis unvollständig. ===== Das Sicherheitsnetz: Tests ===== Ein Test hält fest, was der Code //heute// tut. Nach dem Umbau muss dasselbe herauskommen. # rabatt.py def rabatt(kunde, betrag): if kunde is None: return 0.0 if not kunde["aktiv"]: return 0.0 if betrag < 100: return 0.0 return round(betrag * 0.1, 2) # test_rabatt.py import pytest from rabatt import rabatt AKTIV = {"aktiv": True} GESPERRT = {"aktiv": False} @pytest.mark.parametrize("kunde, betrag, erwartet", [ (None, 200, 0.0), (GESPERRT, 200, 0.0), (AKTIV, 99.99, 0.0), (AKTIV, 100, 10.0), (AKTIV, 250, 25.0), ]) def test_rabatt(kunde, betrag, erwartet): assert rabatt(kunde, betrag) == erwartet $ pytest -q ..... [100%] 5 passed in 0.01s Beachten Sie die Auswahl der Fälle: nicht fünf beliebige Zahlen, sondern **die Grenzen** (99.99 und 100) und **jeder Zweig** der Funktion. Genau dort geht beim Umbau etwas kaputt. ===== Code ohne Tests: Characterization Test ===== In bestehendem Code gibt es meist keine Tests - und Sie wissen nicht, ob das aktuelle Verhalten überhaupt richtig ist. Trotzdem können Sie es festhalten. Ein **Characterization Test** behauptet nicht, dass das Ergebnis //korrekt// ist, sondern nur, dass es //gleich bleibt//. Vorgehen: - Funktion mit realistischen Eingaben aufrufen und die Ausgabe protokollieren. - Diese Ausgabe als erwarteten Wert in den Test schreiben, auch wenn sie seltsam aussieht. - Test laufen lassen: grün. - Refactoren. Bleibt der Test grün, hat sich nichts verändert. def test_bestandsbericht_bleibt_gleich(): # festgehalten am 09.09.2026, vor dem Refactoring assert bestandsbericht(BEISPIELDATEN) == "Total: 215.0 CHF (3 Positionen)" Findet der Test später einen echten Fehler im alten Verhalten: **erst refactoren, dann in einem separaten Commit den Bug beheben** und den erwarteten Wert im Test anpassen. So bleibt nachvollziehbar, welche Änderung was bewirkt hat. ===== Fünf Umbauten, die das Verhalten still verändern ===== Diese Fälle sehen wie Refactorings aus, sind aber keine. Sie gehören zu **D1A** («unerwünschte Nebeneffekte vermeiden»). ==== 1. sorted() durch sort() ersetzt ==== namen = ["Zoe", "Ali", "Bea"] sortiert = sorted(namen) # -> ['Ali', 'Bea', 'Zoe'], namen unverändert sortiert = namen.sort() # -> None, namen ist jetzt sortiert ''sort()'' verändert die Liste und gibt ''None'' zurück. Wer die Zeile «vereinfacht», bekommt einen ''None''-Fehler an ganz anderer Stelle - oder schlimmer: gar keinen Fehler, aber eine veränderte Originalliste. ==== 2. Liste durch Generator ersetzt ==== werte = (n * n for n in range(4)) print(sum(werte)) # 14 print(sum(werte)) # 0 <- der Generator ist aufgebraucht Generatoren sparen Speicher, aber sie sind **einmal** durchlaufbar. Wo das Ergebnis zweimal gebraucht wird (einmal für die Summe, einmal für die Anzeige), muss es eine Liste bleiben. ==== 3. Mutable Default-Wert eingeführt ==== def add(x, ziel=[]): ziel.append(x) return ziel print(add(1)) # [1] print(add(2)) # [1, 2] <- nicht [2] Der Standardwert wird **einmal** beim Definieren der Funktion ausgewertet, nicht bei jedem Aufruf (siehe die Animation in [[modul:m323:learningunits:lu02:byreferenceinclasses|LU02e]]). Richtig ist ''ziel=None'' mit ''ziel = [] if ziel is None else ziel''. ==== 4. Query und Modifier zusammengelegt ==== def entnehmen(bestand, artikel): bestand[artikel] -= 1 return bestand[artikel] lager = {"maus": 3} print(entnehmen(lager, "maus")) # 2 print(entnehmen(lager, "maus")) # 1 Derselbe Aufruf, verschiedene Ergebnisse. Wer diese Funktion beim Refactoring an eine zweite Stelle kopiert, um «nur kurz nachzuschauen», verändert den Bestand. ==== 5. Geteilte Referenz statt Kopie ==== def preisliste_kopieren(original): return original # keine Kopie, dieselbe Liste # return list(original) # flache Kopie Beide Varianten laufen fehlerfrei durch. Der Unterschied zeigt sich erst, wenn jemand die «Kopie» verändert. ===== Ablauf, der Sie schützt ===== - **Vorher committen.** Ein sauberer Stand, zu dem Sie zurückkönnen. - **Tests schreiben oder prüfen.** Grün, bevor Sie irgendetwas anfassen. - **Ein Refactoring, ein Commit.** Commit-Message mit Technik: ''refactor: Extract Function gesamtsumme()''. - **Tests nach jedem Schritt.** Rot heisst: sofort zurück, nicht «schnell noch fixen». - **Diff lesen.** ''git diff'' vor dem Commit - sehen Sie nur, was Sie ändern wollten? **Diese Commit-Historie ist Ihr Portfolio-Nachweis.** Ein Screenshot von ''git diff'' mit dem zugehörigen grünen Testlauf belegt D1I vollständig. Nachträglich lässt sich das nicht rekonstruieren. ===== Werkzeuge ===== ^ Werkzeug ^ Wofür ^ | **PyCharm-Refactorings** | Rename ''Shift+F6'', Extract Method ''Ctrl+Alt+M'', Extract Variable ''Ctrl+Alt+V'' - erfasst alle Aufrufstellen | | **pytest** | Sicherheitsnetz, ''pytest -q'' nach jedem Schritt | | **ruff** oder **flake8** | findet ungenutzte Variablen, tote Importe, zu komplexe Funktionen | | **black** | einheitliche Formatierung, damit der Diff nur echte Änderungen zeigt | | **git** | kleine Commits, ''git diff'', Rückweg zum letzten grünen Stand | ---- {{tag>M323-LU07 M323-D1I M323-D1A}} [[https://creativecommons.org/licenses/by-nc-sa/4.0/ch/|{{https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png}}]] (c) Kevin Maurizi