Warum schreiben Sie Tests für Code, den ich überarbeiten werde?

15

Ich überarbeite eine riesige Legacy-Code-Klasse. Refactoring (ich nehme an) befürwortet dies:

  1. Schreibe Tests für die Legacy-Klasse
  2. 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.

Dennis
quelle
1
Ihre Prämisse ist falsch. Sie werden Ihre Tests nicht ändern. Du wirst neue Tests schreiben. Schritt 3 wird lauten: "Alle Tests löschen, die jetzt nicht mehr vorhanden sind."
pdr
1
In Schritt 3 kann dann "Neue Tests schreiben. Nicht mehr vorhandene Tests löschen" angezeigt werden. Ich denke, es läuft immer noch darauf hinaus, das Original zu zerstören
Dennis
3
Nein, Sie möchten die neuen Tests in Schritt 2 schreiben. Und ja, Schritt 1 wird zerstört. Aber war es Zeitverschwendung? Nein, denn es gibt Ihnen eine Menge Sicherheit, dass Sie in Schritt 2 nichts kaputt machen. Bei Ihren neuen Tests ist dies nicht der Fall.
pdr
3
@Dennis - Ich teile zwar die gleichen Bedenken, die Sie in Bezug auf die Situation haben, aber wir könnten die meisten Umgestaltungsarbeiten als "Zerstörung der Originalarbeit" betrachten. Wenn wir sie jedoch niemals zerstören würden, würden wir uns niemals von Spaghetti-Code mit 10.000 Zeilen in einer Zeile entfernen Datei. Gleiches sollte wahrscheinlich für Unit-Tests gehen, sie gehen Hand in Hand mit dem Code, den sie testen. Wenn sich der Code weiterentwickelt und Dinge verschoben und / oder gelöscht werden, sollten sich auch die Komponententests mit ihm weiterentwickeln.
DXM
"Den Code verstehen" ist kein geringer Vorteil. Wie können Sie ein Programm umgestalten, das Sie nicht verstehen? Es ist unvermeidlich, und es gibt keinen besseren Weg, um das wahre Verständnis eines Programms zu demonstrieren, als einen gründlichen Test zu schreiben. Es sollte auch gesagt werden, dass je abstrakter das Testen ist, desto unwahrscheinlicher wird es sein, dass Sie es später zerkratzen müssen. Wenn überhaupt, sollten Sie sich zunächst an das Testen auf hoher Ebene halten.
Neil

Antworten:

46

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.

amon
quelle
6
+1. Lies meine Gedanken, schrieb meine Antwort. Wichtiger Punkt: Möglicherweise müssen Sie Komponententests schreiben, um zu zeigen, dass die gleichen Fehler nach dem Refactoring immer noch vorhanden sind!
david.pfx
Frage: Warum ändern Sie in Ihrem Beispiel zur Änderung des Funktionsnamens zuerst den Test, um sicherzustellen, dass er fehlschlägt? Ich möchte sagen, dass es natürlich scheitern wird, wenn Sie es ändern - Sie haben die Verbindung unterbrochen, mit der Linker Code zusammenbinden! Erwarten Sie vielleicht, dass es eine weitere private Funktion mit dem gerade neu gewählten Namen gibt, und Sie müssen überprüfen, ob dies nicht der Fall ist, falls Sie sie verpasst haben? Ich sehe, dass dies Ihnen eine gewisse Sicherheit gibt, die an Zwangsstörungen grenzt, aber in diesem Fall fühlt es sich wie ein Overkill an. Gibt es jemals einen möglichen Grund, warum der Test in Ihrem Beispiel nicht fehlschlägt?
Dennis
^ cont: Als allgemeine Technik sehe ich, dass es gut ist, den Code schrittweise auf Fehler zu überprüfen, sobald Sie können. Es mag so sein, dass Sie nicht krank werden, wenn Sie sich nicht jedes Mal die Hände waschen, aber wenn Sie Ihre Hände nur aus Gewohnheit waschen, bleiben Sie insgesamt gesünder, unabhängig davon, ob Sie mit kontaminierten Dingen in Berührung kommen oder nicht. Hier können Sie Ihre Hände manchmal überflüssig waschen oder Code manchmal überflüssig testen, aber es hilft, Sie und Ihren Code gesund zu halten. Ist es das, was du meinst?
Dennis
@ Tennis Eigentlich habe ich unbewusst ein wissenschaftlich korrektes Experiment beschrieben: Wir können nicht sagen, welcher Parameter das Ergebnis tatsächlich beeinflusste, wenn mehr als ein Parameter geändert wurde. Denken Sie daran, dass Tests Code sind und jeder Code Fehler enthält. Wirst du in die Hölle der Programmierer gehen, weil du die Tests nicht ausgeführt hast, bevor du den Code berührt hast? Sicher nicht: Wenn Sie die Tests ausführen, ist es Ihre professionelle Entscheidung, ob dies erforderlich ist. Beachten Sie außerdem, dass ein Test fehlgeschlagen ist, wenn er nicht kompiliert werden kann, und dass meine Antwort auch auf dynamische Sprachen anwendbar ist, nicht nur auf statische Sprachen mit einem Linker.
amon
2
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.
Dennis
7

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?"

Philip
quelle
Ich glaube ich verstehe, aber du hast mich an den Schnittstellen verloren. dh Tests, die ich gerade schreibe, prüfen, ob bestimmte Variablen ordnungsgemäß ausgefüllt wurden, nachdem die zu testende Methode aufgerufen wurde. Wenn diese Variablen geändert oder überarbeitet werden, werden dies auch meine Tests sein. Bestehende Legacy-Klassen, mit denen ich arbeite, haben keine Schnittstellen / Getter / Setter per seh, was variable Änderungen oder solche weniger arbeitsintensiv machen würde. Aber auch hier bin ich mir nicht sicher, was Sie mit Schnittstellen in Bezug auf Legacy-Code meinen. Vielleicht kann ich welche erstellen? Aber das wird umgestalten.
Dennis
1
Ja, wenn Sie eine Gottklasse haben, die alles macht, dann gibt es wirklich keine Schnittstellen. Wenn es jedoch eine andere Klasse aufruft, erwartet die oberste Klasse, dass sie sich auf eine bestimmte Weise verhält, und die Komponententests können dies überprüfen. Trotzdem würde ich nicht so tun, als müssten Sie Ihre Unit-Tests beim Refactoring nicht aktualisieren.
Philip
4

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.

Dennis
quelle
Klingt eher so, als hätten Sie die Anwendung zusammen mit dem Refactoring neu gestaltet.
JeffO
Wann wird überarbeitet und wann neu gestaltet? dh beim Refactoring ist es schwierig, größere unhandliche Klassen nicht in kleinere zu unterteilen und sie auch zu verschieben. Ich bin mir der Unterscheidung nicht ganz sicher, aber vielleicht mache ich beides.
Dennis
3

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.

Michael Durrant
quelle
Ihr erster Absatz scheint zu befürworten, Schritt 1 zu ignorieren und Tests zu schreiben, wie er geht; Ihr zweiter Absatz scheint dem zu widersprechen.
pdr
Aktualisiert meine Antwort.
Michael Durrant
2

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:

  • Variable umbenennen
  • Funktion umbenennen
  • Funktion hinzufügen
  • Löschfunktion
  • Split-Funktion in zwei oder mehr Funktionen
  • kombinieren Sie zwei oder mehr Funktionen zu einer Funktion
  • Split-Klasse
  • Klassen kombinieren
  • Klasse umbenennen

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 an bar()

  • function flee() ruft auch zum Funktionieren auf bar()

  • Nur für Abwechslung, flam()ruft anfoo()

  • Alles funktioniert hervorragend (anscheinend zumindest).

  • Sie refactor ...

  • bar() wird umbenannt in barista()

  • flee() wird geändert, um anzurufen barista()

  • foo()wird nicht geändert, um anzurufenbarista()

Offensichtlich scheitern Ihre Tests für beide foo()und flam()jetzt.

Vielleicht haben Sie gar nicht bemerkt, dass Sie überhaupt foo()angerufen bar()haben. Sie wusste schon gar nicht , dass flam()auf abhing bar()haft foo().

Was auch immer. Der Punkt ist , dass Ihre Tests werden das neu aufgebrochen Verhalten beide aufzudecken foo()und flam(), 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()bricht foo(), dann foo()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.

Craig
quelle