Ich überarbeite eine riesige Legacy-Code-Klasse. Refactoring (ich nehme an) befürwortet dies:
- Schreibe Tests für die Legacy-Klasse
- Refactor zum Teufel aus der Klasse
Problem: Sobald ich die Klasse überarbeitet habe, müssen meine Tests in Schritt 1 geändert werden. Was früher in einer Legacy-Methode enthalten war, kann jetzt stattdessen eine separate Klasse sein. Was eine Methode war, kann jetzt mehrere Methoden sein. Die gesamte Landschaft der Legacy-Klasse wird möglicherweise in etwas Neues verwischt, und die Tests, die ich in Schritt 1 schreibe, sind fast null und nichtig. Im Wesentlichen werde ich Schritt 3 hinzufügen . Schreiben Sie meine Tests gründlich um
Was ist dann der Zweck, Tests vor dem Refactor zu schreiben? Es klingt eher nach einer akademischen Übung, mehr Arbeit für mich selbst zu schaffen. Ich schreibe gerade Tests für die Methode und lerne mehr darüber, wie man Dinge testet und wie die Legacy-Methode funktioniert. Dies kann man lernen, indem man nur den Legacy-Code selbst liest, aber das Schreiben von Tests ist fast so, als würde man sich die Nase reiben und dieses vorübergehende Wissen in separaten Tests dokumentieren. Auf diese Weise habe ich fast keine andere Wahl, als zu lernen, was der Code tut. Ich sagte vorübergehend hier, weil ich den Code überarbeiten werde und alle meine Dokumentationen und Tests für einen bedeutenden Teil ungültig sind, außer mein Wissen wird bleiben und es mir ermöglichen, das Überarbeiten frischer zu gestalten.
Ist das der wahre Grund, Tests vor dem Refactor zu schreiben - damit ich den Code besser verstehe? Es muss einen anderen Grund geben!
Bitte erkläre!
Hinweis:
Es gibt diesen Beitrag: Ist es sinnvoll, Tests für Legacy-Code zu schreiben, wenn keine Zeit für ein vollständiges Refactoring bleibt? Aber es heißt "Tests vor Refactor schreiben", aber es heißt nicht "Warum" oder was zu tun ist, wenn "Tests schreiben" wie "viel zu tun, was bald zerstört wird" aussieht.
quelle
Antworten:
Beim Refactoring wird ein Teil des Codes bereinigt (z. B. der Stil, das Design oder die Algorithmen), ohne dass das (von außen sichtbare) Verhalten geändert wird. Sie schreiben Tests nicht sicher zu stellen , dass der Code vor und nach dem Refactoring ist die gleiche, anstatt Sie Tests als Indikator schreiben , dass Ihre Anwendung vor und nach dem Refactoring verhält sich das gleiche: Der neue Code ist kompatibel, und keine neuen Fehler eingeführt wurden.
Ihr Hauptanliegen sollte es sein, Komponententests für die öffentliche Schnittstelle Ihrer Software zu schreiben. Diese Schnittstelle sollte sich nicht ändern, daher sollten sich auch die Tests (die eine automatische Prüfung für diese Schnittstelle darstellen) nicht ändern.
Tests sind jedoch auch nützlich, um Fehler zu lokalisieren. Daher kann es sinnvoll sein, Tests auch für private Teile Ihrer Software zu schreiben. Es wird erwartet, dass sich diese Tests während des Refactorings ändern. Wenn Sie ein Implementierungsdetail ändern möchten (z. B. die Benennung einer privaten Funktion), aktualisieren Sie zuerst die Tests, um Ihre geänderten Erwartungen widerzuspiegeln. Stellen Sie dann sicher, dass der Test fehlschlägt (Ihre Erwartungen werden nicht erfüllt), und ändern Sie dann den tatsächlichen Code und überprüfen Sie, ob alle Tests erneut bestanden wurden. Zu keinem Zeitpunkt sollten die Tests für die öffentliche Schnittstelle fehlschlagen.
Dies ist schwieriger, wenn Sie Änderungen in größerem Maßstab vornehmen, z. B. mehrere codeabhängige Teile neu entwerfen. Aber es wird eine Art Grenze geben, und an dieser Grenze können Sie Tests schreiben.
quelle
Ah, die Wartung von Altsystemen.
Im Idealfall behandeln Ihre Tests die Klasse nur über ihre Schnittstelle mit der übrigen Codebasis, anderen Systemen und / oder der Benutzeroberfläche. Schnittstellen. Sie können die Schnittstelle nicht umgestalten, ohne die vor- oder nachgelagerten Komponenten zu beeinflussen. Wenn es sich nur um ein eng verbundenes Durcheinander handelt, können Sie den Aufwand genauso gut für ein Umschreiben und nicht für ein Refactoring halten, aber es ist größtenteils semantisch.
Bearbeiten: Nehmen wir an, ein Teil Ihres Codes misst etwas und hat eine Funktion, die einfach einen Wert zurückgibt. Die einzige Schnittstelle ruft die Funktion / Methode / whatnot auf und empfängt den zurückgegebenen Wert. Dies ist eine lose Kupplung und einfach zu einem Komponententest. Wenn Ihr Hauptprogramm eine Unterkomponente hat, die einen Puffer verwaltet, und alle Aufrufe davon abhängen, ob der Puffer selbst oder einige Steuervariablen vorhanden sind, und Fehlermeldungen über einen anderen Codeabschnitt zurückgesetzt werden, kann man sagen, dass dies eng miteinander verbunden ist schwer zu Unit-Test. Sie können es immer noch mit genügend Scheinobjekten und so weiter tun, aber es wird chaotisch. Besonders in c. Jede Art von Umgestaltung, wie der Puffer funktioniert, zerstört die Unterkomponente.
Bearbeiten beenden
Wenn Sie Ihre Klasse über Schnittstellen testen, die stabil bleiben, sollten Ihre Tests vor und nach dem Refactoring gültig sein. Auf diese Weise können Sie sicher sein, dass Sie keine Änderungen vorgenommen haben. Zumindest mehr Selbstvertrauen.
Außerdem können Sie inkrementelle Änderungen vornehmen. Wenn es sich um ein großes Projekt handelt, sollten Sie nicht einfach alles abreißen, ein brandneues System aufbauen und dann mit der Entwicklung von Tests beginnen. Sie können einen Teil davon ändern, testen und sicherstellen, dass die Änderung den Rest des Systems nicht zum Erliegen bringt. Oder wenn ja, können Sie zumindest sehen, wie sich das riesige Wirrwarr entwickelt, anstatt von ihm überrascht zu werden, wenn Sie loslassen.
Sie können eine Methode zwar in drei Teile aufteilen, sie werden jedoch immer noch das Gleiche tun wie bei der vorherigen Methode. Sie können also den Test für die alte Methode durchführen und die Methode in drei Teile aufteilen. Die Mühe, den ersten Test zu schreiben, wird nicht verschwendet.
Auch das Wissen über das Altsystem als "temporäres Wissen" zu behandeln, wird nicht gut gehen. Wenn es um Legacy-Systeme geht, ist es wichtig zu wissen, wie es zuvor funktioniert hat. Sehr nützlich für die uralte Frage "Warum zum Teufel macht es das?"
quelle
Meine eigene Antwort / Erkenntnis:
Durch die Behebung verschiedener Fehler beim Refactoring wird mir klar, dass ich den Code ohne Tests nicht so einfach hätte verschieben können. Tests machen mich auf Verhaltens- / Funktionsunterschiede aufmerksam, die ich durch Ändern meines Codes einführe.
Sie müssen nicht überbewusst sein, wenn Sie gute Tests haben. Sie können Ihren Code entspannter bearbeiten. Tests führen die Überprüfung und die Überprüfung der Gesundheit für Sie durch.
Außerdem sind meine Tests so gut wie gleich geblieben, als ich sie überarbeitet habe und sie wurden nicht zerstört. Ich habe tatsächlich einige zusätzliche Gelegenheiten bemerkt, um meinen Tests Zusicherungen hinzuzufügen, während ich mich eingehender mit Code befasste.
AKTUALISIEREN
Nun, jetzt ändere ich meine Tests sehr: / Weil ich die ursprüngliche Funktion überarbeitet habe (die Funktion entfernt und stattdessen eine neue Reinigerklasse erstellt, wobei die Flusen, die sich früher in der Funktion befanden, außerhalb der neuen Klasse verschoben wurden), also jetzt Der Code, den ich zuvor ausgeführt habe, nimmt verschiedene Parameter unter einem anderen Klassennamen auf und führt zu unterschiedlichen Ergebnissen (der ursprüngliche Code mit dem Flaum hatte mehr zu testende Ergebnisse). Daher müssen meine Tests diese Änderungen widerspiegeln, und im Grunde schreibe ich meine Tests in etwas Neues um.
Ich nehme an, es gibt andere Lösungen, die ich tun kann, um das Umschreiben von Tests zu vermeiden. zB den alten Funktionsnamen mit neuem Code und dem Flaum darin behalten ... aber ich weiß nicht, ob es die beste Idee ist und ich habe noch nicht so viel Erfahrung, um ein Urteil darüber zu fällen, was zu tun ist.
quelle
Verwenden Sie Ihre Tests, um Ihren Code so zu steuern, wie Sie es tun. In dem von Legacy-Code bedeutet dies, Tests für den Code zu schreiben, den Sie ändern werden. Auf diese Weise sind sie kein separates Artefakt. Bei Tests sollte es darum gehen, was der Code erreichen soll, und nicht darum, wie er es tut.
Im Allgemeinen möchten Sie Tests für Code hinzufügen, der keinen hat.) Für Code, den Sie umgestalten möchten, um sicherzustellen, dass das Verhalten des Codes weiterhin wie erwartet funktioniert. Das kontinuierliche Ausführen der Testsuite während des Refactorings ist daher ein fantastisches Sicherheitsnetz. Der Gedanke, Code ohne eine Testsuite zu ändern, um zu bestätigen, dass sich die Änderungen nicht auf etwas Unerwartetes auswirken, ist beängstigend.
Was die Aktualisierung alter Tests, das Schreiben neuer Tests, das Löschen alter Tests usw. angeht, sehe ich das nur als Teil der Kosten für die moderne professionelle Softwareentwicklung.
quelle
Was ist das Ziel von Refactoring in Ihrem speziellen Fall?
Um meine Antwort zu ertragen, nehmen wir an, dass wir alle (bis zu einem gewissen Grad) an TDD (Test-Driven Development) glauben.
Wenn der Zweck Ihres Refactorings darin besteht, vorhandenen Code zu bereinigen, ohne das vorhandene Verhalten zu ändern, stellen Sie durch Schreiben von Tests vor dem Refactoring sicher, dass Sie das Verhalten des Codes nicht geändert haben. Wenn Sie erfolgreich sind, sind die Tests sowohl vorher als auch nachher erfolgreich Sie refactor.
Mithilfe der Tests können Sie sicherstellen, dass Ihre neue Arbeit tatsächlich funktioniert.
Die Tests werden wahrscheinlich auch Fälle aufdecken, in denen das Originalwerk nicht funktioniert.
Aber wie können Sie wirklich signifikante Umgestaltungen vornehmen, ohne das Verhalten in gewissem Maße zu beeinträchtigen ?
Hier ist eine kurze Liste einiger Dinge, die beim Refactoring passieren können:
Ich werde argumentieren, dass jede einzelne dieser aufgeführten Aktivitäten das Verhalten in irgendeiner Weise verändert.
Und ich werde argumentieren, dass, wenn sich Ihr Refactoring-Verhalten ändert, Ihre Tests immer noch die Art und Weise sein werden, wie Sie sicherstellen, dass Sie nichts kaputt gemacht haben.
Vielleicht ändert sich das Verhalten auf Makroebene nicht, aber der Punkt des Unit- Tests besteht nicht darin, das Makroverhalten sicherzustellen. Das ist Integrationstest . Der Zweck des Komponententests besteht darin, sicherzustellen, dass die einzelnen Teile, aus denen Sie Ihr Produkt bauen, nicht beschädigt werden. Kette, schwächstes Glied usw.
Wie wäre es mit diesem Szenario:
Angenommen, Sie haben
function bar()
function foo()
ruft anbar()
function flee()
ruft auch zum Funktionieren aufbar()
Nur für Abwechslung,
flam()
ruft anfoo()
Alles funktioniert hervorragend (anscheinend zumindest).
Sie refactor ...
bar()
wird umbenannt inbarista()
flee()
wird geändert, um anzurufenbarista()
foo()
wird nicht geändert, um anzurufenbarista()
Offensichtlich scheitern Ihre Tests für beide
foo()
undflam()
jetzt.Vielleicht haben Sie gar nicht bemerkt, dass Sie überhaupt
foo()
angerufenbar()
haben. Sie wusste schon gar nicht , dassflam()
auf abhingbar()
haftfoo()
.Was auch immer. Der Punkt ist , dass Ihre Tests werden das neu aufgebrochen Verhalten beide aufzudecken
foo()
undflam()
, in einer inkrementellen Art und Weise während der Refactoring Arbeit.Die Tests helfen Ihnen letztendlich dabei, das Produkt gut umzugestalten.
Es sei denn, Sie haben keine Tests.
Das ist ein bisschen ein ausgedachtes Beispiel. Es gibt diejenigen , die argumentieren , dass , wenn Wechsel
bar()
brichtfoo()
, dannfoo()
zu komplex waren , mit zu beginnen und abgebaut werden soll. Aber Prozeduren können andere Prozeduren aus einem bestimmten Grund aufrufen, und es ist unmöglich, die gesamte Komplexität zu beseitigen , oder? Unsere Aufgabe ist es, die Komplexität einigermaßen gut zu managen .Stellen Sie sich ein anderes Szenario vor.
Sie bauen ein Gebäude.
Sie bauen ein Gerüst, um sicherzustellen, dass das Gebäude ordnungsgemäß gebaut wird.
Das Gerüst hilft Ihnen unter anderem beim Bau eines Aufzugsschachts. Anschließend reißen Sie das Gerüst ab, der Aufzugsschacht bleibt jedoch erhalten. Sie haben "Originalarbeit" zerstört, indem Sie das Gerüst zerstört haben.
Die Analogie ist dürftig, aber der Punkt ist, dass es nicht ungewöhnlich ist, Tools zu erstellen, die Ihnen beim Erstellen von Produkten helfen. Auch wenn die Werkzeuge nicht permanent sind, sind sie nützlich (sogar notwendig). Tischler stellen ständig Vorrichtungen her, manchmal nur für einen Job. Dann reißen sie die Vorrichtungen auseinander, manchmal verwenden sie die Teile, um andere Vorrichtungen für andere Arbeiten zu bauen, manchmal nicht. Aber das macht die Vorrichtungen nicht nutzlos oder unnötig.
quelle