Normalerweise versuche ich, den Ratschlägen des Buches Working Effectively with Legacy Cod e zu folgen . Ich unterbreche Abhängigkeiten, verschiebe Teile des Codes in @VisibleForTesting public static
Methoden und in neue Klassen, um den Code (oder zumindest einen Teil davon) testbar zu machen. Und ich schreibe Tests, um sicherzustellen, dass ich beim Ändern oder Hinzufügen neuer Funktionen nichts kaputt mache.
Ein Kollege sagt, dass ich das nicht tun soll. Seine Argumentation:
- Der ursprüngliche Code funktioniert möglicherweise überhaupt nicht richtig. Und das Schreiben von Tests macht zukünftige Korrekturen und Änderungen schwieriger, da Entwickler die Tests auch verstehen und modifizieren müssen.
- Wenn es sich um GUI-Code mit einer gewissen Logik handelt (z. B. ~ 12 Zeilen, 2-3 if / else-Block), ist ein Test die Mühe nicht wert, da der Code zu trivial ist, um damit zu beginnen.
- Ähnliche schlechte Muster könnten auch in anderen Teilen der Codebasis existieren (was ich noch nicht gesehen habe, ich bin ziemlich neu); es wird einfacher sein, sie alle in einem großen Refactoring aufzuräumen. Das Herausziehen von Logik könnte diese zukünftige Möglichkeit untergraben.
Sollte ich vermeiden, prüfbare Teile herauszunehmen und Tests zu schreiben, wenn wir keine Zeit für eine vollständige Überarbeitung haben? Gibt es einen Nachteil, den ich berücksichtigen sollte?
Antworten:
Hier ist mein persönlicher unwissenschaftlicher Eindruck: Alle drei Gründe klingen nach weit verbreiteten, aber falschen kognitiven Illusionen.
quelle
Ein paar Gedanken:
Wenn Sie älteren Code überarbeiten, spielt es keine Rolle, ob einige der Tests, die Sie schreiben, den idealen Spezifikationen widersprechen. Was zählt, ist, dass sie das aktuelle Verhalten des Programms testen . Beim Refactoring geht es darum, winzige isofunktionale Schritte zu unternehmen, um den Code sauberer zu machen. Sie möchten sich nicht auf Bugfixes einlassen, während Sie umgestalten. Außerdem, wenn Sie einen offensichtlichen Bug entdecken, geht dieser nicht verloren. Sie können jederzeit einen Regressionstest schreiben und diesen vorübergehend deaktivieren oder eine Bugfix-Aufgabe für später in Ihr Backlog einfügen. Eins nach dem Anderen.
Ich bin damit einverstanden, dass reiner GUI-Code schwer zu testen ist und möglicherweise nicht für Refactoring im Stil " Effektiv arbeiten ... " geeignet ist. Dies bedeutet jedoch nicht, dass Sie kein Verhalten extrahieren sollten, das nichts mit der GUI-Ebene zu tun hat, und den extrahierten Code testen sollten. Und "12 Zeilen, 2-3 if / else-Block" ist nicht trivial. Jeder Code mit mindestens ein bisschen Bedingungslogik sollte getestet werden.
Nach meiner Erfahrung sind große Umbauten nicht einfach und funktionieren nur selten. Wenn Sie sich keine genauen, winzigen Ziele setzen, besteht ein hohes Risiko, dass Sie sich auf eine endlose, haarsträubende Überarbeitung einlassen, bei der Sie am Ende nie auf den Beinen landen. Je größer die Veränderung ist, desto größer ist das Risiko, dass Sie etwas kaputtmachen, und desto mehr Schwierigkeiten haben Sie, herauszufinden, wo Sie versagt haben.
Mit kleinen Ad-hoc-Nachbesserungen die Dinge immer besser zu machen, bedeutet nicht, "zukünftige Möglichkeiten zu untergraben", sondern sie zu aktivieren - den sumpfigen Boden zu festigen, auf dem Ihre Anwendung liegt. Sie sollten es auf jeden Fall tun.
quelle
Betreff: "Der ursprüngliche Code funktioniert möglicherweise nicht richtig" - das bedeutet nicht, dass Sie das Verhalten des Codes ändern, ohne sich Gedanken über die Auswirkungen zu machen. Bei anderen Codes kann es sich um fehlerhaftes Verhalten oder um Nebenwirkungen der aktuellen Implementierung handeln. Die Testabdeckung der vorhandenen Anwendung sollte die spätere Umgestaltung erleichtern, da Sie auf diese Weise herausfinden können, wenn Sie versehentlich etwas kaputt gemacht haben. Sie sollten zuerst die wichtigsten Teile testen.
quelle
Kilians Antwort deckt die wichtigsten Aspekte ab, aber ich möchte auf die Punkte 1 und 3 eingehen.
Wenn ein Entwickler Code ändern (umgestalten, erweitern, debuggen) möchte, muss er ihn verstehen. Sie muss sicherstellen, dass sich ihre Änderungen genau auf das von ihr gewünschte Verhalten auswirken (nichts im Falle von Refactoring) und nichts anderes.
Wenn es Tests gibt, muss sie die Tests natürlich auch verstehen. Gleichzeitig sollten die Tests ihr helfen, den Hauptcode zu verstehen, und die Tests sind ohnehin viel einfacher zu verstehen als der Funktionscode (es sei denn, es handelt sich um schlechte Tests). Und die Tests zeigen, was sich am Verhalten des alten Codes geändert hat. Selbst wenn der ursprüngliche Code falsch ist und der Test das falsche Verhalten überprüft, ist dies immer noch ein Vorteil.
Dies setzt jedoch voraus, dass die Tests als Tests für bereits vorhandenes Verhalten und nicht als Spezifikation dokumentiert werden.
Einige Gedanken auch zu Punkt 3: Zusätzlich zu der Tatsache, dass der "Big Swoop" selten tatsächlich auftritt, gibt es noch eine andere Sache: Es ist eigentlich nicht einfacher. Um einfacher zu sein, müssten mehrere Bedingungen zutreffen:
XYZSingleton
? Wird ihr Instanz-Getter immer aufgerufengetInstance()
? Und wie findest du deine zu tiefen Hierarchien? Wie suchst du nach deinen Gottobjekten? Diese erfordern eine Codemetrikanalyse und eine manuelle Überprüfung der Metriken. Oder du stolperst einfach über sie, während du arbeitest, wie du es getan hast.quelle
In einigen Unternehmen gibt es eine Kultur, in der Entwickler sich zurückhalten, Code zu verbessern, der keinen direkten Mehrwert bietet, z. B. neue Funktionen.
Ich predige wahrscheinlich zu den hier Konvertierten, aber das ist eindeutig eine falsche Wirtschaft. Sauberer und präziser Code kommt nachfolgenden Entwicklern zugute. Es ist nur so, dass die Amortisation nicht sofort ersichtlich ist.
Ich persönlich unterschreibe das Pfadfinderprinzip, aber andere (wie Sie gesehen haben) nicht.
Software leidet jedoch unter Entropie und baut technische Schulden auf. Frühere Entwickler, die wenig Zeit hatten (oder vielleicht nur faul oder unerfahren waren), haben möglicherweise suboptimale Buggy-Lösungen gegenüber gut gestalteten Lösungen implementiert. Obwohl es wünschenswert erscheinen mag, diese zu überarbeiten, riskieren Sie, neue Fehler in den (für die Benutzer ohnehin) funktionierenden Code einzufügen.
Einige Änderungen sind risikoärmer als andere. Wenn ich zum Beispiel arbeite, gibt es in der Regel viele duplizierte Codes, die mit minimaler Auswirkung sicher in eine Unterroutine ausgelagert werden können.
Letztendlich müssen Sie ein Urteil darüber fällen, wie weit Sie das Refactoring bringen, aber es ist zweifellos sinnvoll, automatisierte Tests hinzuzufügen, wenn diese noch nicht existieren.
quelle
Nach meiner Erfahrung funktioniert eine Art Charakterisierungstest gut. Es bietet Ihnen relativ schnell eine breite, aber nicht sehr spezifische Testabdeckung, kann jedoch für GUI-Anwendungen schwierig zu implementieren sein.
Ich würde dann Komponententests für Teile schreiben, die Sie ändern möchten, und dies jedes Mal, wenn Sie eine Änderung vornehmen möchten, wodurch sich die Abdeckung der Komponententests mit der Zeit erhöht.
Mit diesem Ansatz erhalten Sie eine gute Vorstellung davon, ob Änderungen andere Teile des Systems betreffen, und können erforderliche Änderungen schneller vornehmen.
quelle
Betreff: "Der ursprüngliche Code funktioniert möglicherweise nicht richtig":
Tests sind nicht in Stein gemeißelt. Sie können geändert werden. Und wenn Sie auf ein falsches Feature getestet haben, sollte es einfach sein, den Test korrekter umzuschreiben. Schließlich sollte sich nur das erwartete Ergebnis der getesteten Funktion geändert haben.
quelle
Nun ja. Beantwortung als Software-Testingenieur. Zunächst sollten Sie alles testen, was Sie jemals getan haben. Denn wenn Sie dies nicht tun, wissen Sie nicht, ob es funktioniert oder nicht. Das mag uns offensichtlich erscheinen, aber ich habe Kollegen, die das anders sehen. Auch wenn es sich bei Ihrem Projekt um ein kleines Projekt handelt, das möglicherweise nie fertiggestellt wird, müssen Sie dem Benutzer ins Gesicht sehen und sagen, dass Sie wissen, dass es funktioniert, weil Sie es getestet haben.
Nicht-trivialer Code enthält immer Bugs (zitiert einen Typen von Uni; und wenn es keine Bugs gibt, ist es trivial) und unsere Aufgabe ist es, diese vor dem Kunden zu finden. Legacy-Code weist Legacy-Fehler auf. Wenn der ursprüngliche Code nicht so funktioniert, wie er sollte, glauben Sie mir. Bugs sind in Ordnung, wenn Sie sie kennen, haben Sie keine Angst, sie zu finden. Dafür sind Release Notes gedacht.
Wenn ich mich recht erinnere, heißt es im Refactoring-Buch, dass man sowieso ständig testen muss. Es ist also Teil des Prozesses.
quelle
Führen Sie die automatisierte Testabdeckung durch.
Passen Sie auf Wunschdenken auf, sowohl von Ihnen selbst als auch von Ihren Kunden und Vorgesetzten. So sehr ich auch gerne glauben möchte, dass meine Änderungen beim ersten Mal korrekt sind und ich sie nur einmal testen muss, so habe ich gelernt, diese Art des Denkens genauso zu behandeln wie nigerianische Betrugs-E-Mails. Meistens; Ich habe noch nie eine betrügerische E-Mail erhalten, habe aber kürzlich (als ich angeschrien habe) nachgegeben, dass ich keine Best Practices verwende. Es war eine schmerzhafte Erfahrung, die sich (teuer) hin und her zog. Nie wieder!
Ich habe ein Lieblingszitat aus dem Freefall-Webcomic: "Haben Sie jemals in einem komplexen Bereich gearbeitet, in dem der Supervisor nur eine ungefähre Vorstellung von den technischen Details hat? ... Dann wissen Sie, dass es der sicherste Weg ist, Ihren Supervisor zum Scheitern zu bringen befolgen Sie jede Bestellung ohne Frage. "
Es ist wahrscheinlich angebracht, die Zeit zu begrenzen, die Sie investieren.
quelle
Wenn Sie mit einer großen Menge von Legacy-Code zu tun haben, der derzeit nicht getestet wird, ist es die richtige Entscheidung, jetzt eine Testabdeckung zu erhalten, anstatt in Zukunft auf eine hypothetische umfassende Neuschreibung zu warten. Mit dem Schreiben von Unit-Tests zu beginnen, ist nicht.
Ohne automatisierte Tests müssen Sie nach Änderungen am Code einige manuelle End-to-End-Tests der App durchführen, um sicherzustellen, dass sie funktioniert. Beginnen Sie damit, Integrationstests auf hoher Ebene zu schreiben, um diese zu ersetzen. Wenn Ihre App Dateien einliest, validiert, die Daten auf irgendeine Weise verarbeitet und die Ergebnisse anzeigt, die Sie mit Tests erfassen möchten.
Idealerweise verfügen Sie entweder über Daten aus einem manuellen Testplan oder können ein Beispiel der tatsächlichen Produktionsdaten zur Verwendung abrufen. Wenn nicht, da die App in der Produktion ist, macht sie in den meisten Fällen das, was sie sein sollte, also erstelle Daten, die alle Höhepunkte erreichen und nehme an, dass die Ausgabe vorerst korrekt ist. Es ist nicht schlimmer, als eine kleine Funktion zu übernehmen, vorausgesetzt, sie tut, wie es heißt, oder Kommentare legen nahe, dass dies der Fall ist, und Tests zu schreiben, vorausgesetzt, sie funktioniert ordnungsgemäß.
Wenn Sie genug von diesen Tests auf hoher Ebene haben, um den normalen Betrieb der Apps und die häufigsten Fehlerfälle zu erfassen, müssen Sie viel Zeit auf der Tastatur verbringen, um zu versuchen, Fehler aus dem Code herauszufinden, die etwas anderes tun als was Sie dachten, dass dies zu einem erheblichen Rückgang führen würde, was das zukünftige Refactoring (oder sogar ein umfangreiches Umschreiben) erheblich vereinfacht.
Wenn Sie die Abdeckung der Komponententests erweitern können, können Sie die meisten Integrationstests reduzieren oder sogar einstellen. Wenn Ihre App Dateien liest / schreibt oder auf eine Datenbank zugreift, ist es naheliegend, diese Teile einzeln zu testen und sie entweder zu verspotten oder die Datenstrukturen zu erstellen, die aus der Datei / Datenbank gelesen werden. Das eigentliche Erstellen dieser Testinfrastruktur dauert viel länger als das Schreiben einer Reihe schneller und unsauberer Tests. und jedes Mal, wenn Sie einen 2-minütigen Satz von Integrationstests ausführen, anstatt 30 Minuten manuell zu verbringen, um einen Bruchteil dessen zu testen, was die Integrationstests abgedeckt haben, erzielen Sie bereits einen großen Gewinn.
quelle