In einer anderen Frage wurde herausgefunden, dass eines der Probleme mit TDD darin besteht, die Testsuite während und nach dem Refactoring mit der Codebasis synchron zu halten.
Jetzt bin ich ein großer Fan von Refactoring. Ich werde es nicht aufgeben, um TDD zu machen. Aber ich habe auch die Probleme von Tests erlebt, die so geschrieben wurden, dass geringfügiges Refactoring zu vielen Testfehlern führt.
Wie vermeidet man das Brechen von Tests beim Refactoring?
- Schreiben Sie die Tests "besser"? Wenn ja, worauf sollten Sie achten?
- Vermeiden Sie bestimmte Arten von Refactoring?
- Gibt es Test-Refactoring-Tools?
Bearbeiten: Ich schrieb eine neue Frage , die fragte, was ich fragen wollte (aber diese als interessante Variante beibehalten).
development-process
tdd
refactoring
Alex Feinman
quelle
quelle
Antworten:
Was Sie versuchen zu tun, ist nicht wirklich umgestalten. Mit Refactoring, per Definition, die Sie nicht ändern , was eine Software macht, ändern Sie , wie sie es tut.
Beginnen Sie mit allen grünen Tests (alle bestanden), und nehmen Sie dann Änderungen "unter der Haube" vor (z. B. Verschieben einer Methode aus einer abgeleiteten Klasse in die Basis, Extrahieren einer Methode oder Kapseln eines Verbunds mit einem Builder usw.). Ihre Tests sollten noch bestehen.
Was Sie beschreiben, scheint kein Refactoring zu sein, sondern ein Redesign, das auch die Funktionalität Ihrer getesteten Software erweitert. TDD und Refactoring (wie ich es hier zu definieren versuchte) stehen nicht in Konflikt. Sie können weiterhin eine Umgestaltung (Grün-Grün) vornehmen und TDD (Rot-Grün) anwenden, um die "Delta" -Funktionalität zu entwickeln.
quelle
Einer der Vorteile von Komponententests besteht darin, dass Sie sicher überarbeiten können.
Wenn das Refactoring die öffentliche Schnittstelle nicht verändert, lassen Sie die Unit-Tests unverändert und stellen nach dem Refactoring sicher, dass sie alle bestanden haben.
Wenn das Refactoring die öffentliche Schnittstelle ändert, sollten die Tests zuerst neu geschrieben werden. Refactor bis die neuen Tests bestanden sind.
Ich würde niemals ein Refactoring vermeiden, weil es die Tests bricht. Schreiben von Unit-Tests kann ein Schmerz in einem Hintern sein, aber es ist den Schmerz auf lange Sicht wert.
quelle
Im Gegensatz zu den anderen Antworten ist es wichtig zu beachten, dass einige Testmethoden zerbrechlich werden können , wenn das zu testende System (SUT) überarbeitet wird, wenn es sich bei dem Test um eine Whitebox handelt.
Wenn ich ein Mocking-Framework verwende, das die Reihenfolge der Methoden überprüft, die für die Mocks aufgerufen wurden (wenn die Reihenfolge irrelevant ist, weil die Aufrufe frei von Nebenwirkungen sind); Wenn mein Code mit diesen Methodenaufrufen in einer anderen Reihenfolge sauberer ist und ich eine Umgestaltung vornehme, bricht mein Test ab. Im Allgemeinen können Verspottungen zu fragilen Tests führen.
Wenn ich den internen Status meines SUT überprüfe, indem ich seine privaten oder geschützten Mitglieder offenlege (wir könnten "friend" in Visual Basic verwenden oder die Zugriffsebene "internal" eskalieren und "internalsvisibleto" in C #; in vielen OO-Sprachen verwenden, einschließlich c # eine " testspezifische Unterklasse " könnte verwendet werden), dann ist plötzlich der interne Status der Klasse von Bedeutung - Sie überarbeiten die Klasse möglicherweise als Black-Box, aber White-Box-Tests schlagen fehl. Angenommen, ein einzelnes Feld wird wiederverwendet, um verschiedene Bedeutungen zu haben (keine gute Praxis!), Wenn sich der Status des SUT ändert. Wenn wir es in zwei Felder aufteilen, müssen wir möglicherweise fehlerhafte Tests neu schreiben.
Testspezifische Unterklassen können auch zum Testen geschützter Methoden verwendet werden. Dies kann bedeuten, dass ein Refaktor aus Sicht des Produktionscodes eine grundlegende Änderung aus Sicht des Testcodes darstellt. Das Verschieben einiger Zeilen in eine geschützte Methode oder aus einer geschützten Methode heraus hat möglicherweise keine produktionsbedingten Nebenwirkungen, unterbricht jedoch einen Test.
Wenn ich " Test Hooks " oder einen anderen testspezifischen oder bedingten Kompilierungscode verwende, kann es schwierig sein, sicherzustellen, dass die Tests nicht aufgrund fragiler Abhängigkeiten von der internen Logik unterbrochen werden.
Um zu verhindern, dass Tests an die intimen internen Details des SUT gekoppelt werden, kann Folgendes hilfreich sein:
Alle obigen Punkte sind Beispiele für die in Tests verwendete White-Box-Kopplung. Verwenden Sie Black-Box-Tests des SUT, um die Umgestaltung von Bruchtests vollständig zu vermeiden.
Haftungsausschluss: Um hier das Refactoring zu diskutieren, verwende ich das Wort etwas weiter, um die Änderung der internen Implementierung ohne sichtbare externe Auswirkungen einzuschließen. Einige Puristen stimmen möglicherweise nicht überein und beziehen sich ausschließlich auf das Buch Refactoring von Martin Fowler und Kent Beck - das atomare Refactoring-Operationen beschreibt.
In der Praxis tendieren wir dazu, geringfügig größere Schritte zur Aufrechterhaltung der Unterbrechungsfreiheit als die dort beschriebenen atomaren Operationen auszuführen, und insbesondere Änderungen, die dazu führen, dass sich der Produktionscode von außen identisch verhält, führen möglicherweise nicht zum Bestehen von Tests. Aber ich denke, es ist fair, "Ersatzalgorithmus für einen anderen Algorithmus mit identischem Verhalten" als Refaktor aufzunehmen, und ich denke, Fowler stimmt dem zu. Martin Fowler selbst sagt, dass Refactoring Tests brechen kann:
quelle
Wenn Ihre Tests beim Refactoring abbrechen, dann ist dies definitionsgemäß kein Refactoring, was bedeutet, "die Struktur Ihres Programms zu ändern, ohne das Verhalten Ihres Programms zu ändern".
Manchmal müssen Sie das Verhalten Ihrer Tests ändern. Möglicherweise müssen Sie zwei Methoden zusammenführen (z. B. bind () und listen () für eine empfangsbereite TCP-Socket-Klasse), damit andere Teile Ihres Codes versuchen, die jetzt geänderte API zu verwenden. Aber das ist kein Refactoring!
quelle
Ich denke, das Problem bei dieser Frage ist, dass verschiedene Leute das Wort "Refactoring" unterschiedlich verstehen. Ich denke, es ist am besten, ein paar Dinge, die Sie wahrscheinlich meinen, sorgfältig zu definieren:
Wie eine andere Person bereits bemerkt hat, sollten Sie keine Probleme haben, wenn Sie die API unverändert lassen und alle Ihre Regressionstests auf der öffentlichen API ausgeführt werden. Refactoring sollte überhaupt keine Probleme verursachen. Alle fehlgeschlagenen Tests WEDER bedeuten, dass Ihr alter Code einen Fehler hatte und Ihr Test schlecht ist, oder Ihr neuer Code einen Fehler hat.
Aber das ist ziemlich offensichtlich. Sie meinen also mit Refactoring, dass Sie die API ändern.
Lassen Sie mich also antworten, wie ich das angehen soll!
Erstellen Sie zunächst eine NEUE API, die genau das tut, was Sie für Ihr NEUES API-Verhalten wünschen. Wenn diese neue API den gleichen Namen wie eine ältere API hat, füge ich den Namen _NEW an den neuen API-Namen an.
int DoSomethingInterestingAPI ();
wird:
OK, zu diesem Zeitpunkt sind alle Ihre Regressionstests noch nicht bestanden. Verwenden Sie dazu den Namen DoSomethingInterestingAPI ().
WEITER, gehen Sie Ihren Code durch und ändern Sie alle Aufrufe von DoSomethingInterestingAPI () in die entsprechende Variante von DoSomethingInterestingAPI_NEW (). Dies beinhaltet das Aktualisieren / Umschreiben aller Teile Ihrer Regressionstests, die geändert werden müssen, um die neue API zu verwenden.
Als nächstes markieren Sie DoSomethingInterestingAPI_OLD () als [[veraltet ()]. Behalten Sie die veraltete API so lange bei, wie Sie möchten (bis Sie den gesamten Code, der möglicherweise davon abhängt, sicher aktualisiert haben).
Bei diesem Ansatz sind alle Fehler in Ihren Regressionstests einfach Fehler in diesem Regressionstest oder identifizieren Fehler in Ihrem Code - genau so, wie Sie es möchten. Dieser schrittweise Prozess der Überarbeitung einer API durch explizites Erstellen von _NEW- und _OLD-Versionen der API ermöglicht es Ihnen, Teile des neuen und alten Codes für eine Weile nebeneinander zu haben.
quelle
Ich gehe davon aus, dass Ihre Komponententests eine Granularität haben, die ich als "dumm" bezeichnen würde. Das heißt, sie testen die absoluten Minutien jeder Klasse und Funktion. Entfernen Sie sich von den Code-Generator-Tools und schreiben Sie Tests, die auf eine größere Oberfläche angewendet werden. Dann können Sie die Interna beliebig umgestalten, da Sie wissen, dass sich die Schnittstellen zu Ihren Anwendungen nicht geändert haben und Ihre Tests weiterhin funktionieren.
Wenn Sie Komponententests haben möchten, bei denen jede Methode getestet wird, müssen Sie sie gleichzeitig überarbeiten.
quelle
Was es schwierig macht, ist die Kopplung . Alle Tests sind in gewissem Maße an Implementierungsdetails gekoppelt, aber Unit-Tests (unabhängig davon, ob es sich um TDD handelt oder nicht) sind besonders schlecht, da sie die Interna stören: Mehr Unit-Tests bedeuten mehr an Units gekoppelten Code, dh Methodensignaturen / jede andere öffentliche Schnittstelle von Einheiten - zumindest.
"Einheiten" sind per Definition Implementierungsdetails auf niedriger Ebene. Die Schnittstelle von Einheiten kann und sollte sich ändern, aufteilen, zusammenführen und auf andere Weise mutieren, wenn sich das System weiterentwickelt. Eine Fülle von Komponententests kann diese Entwicklung tatsächlich mehr behindern, als sie hilft.
Wie vermeide ich das Brechen von Tests beim Refactoring? Kupplung vermeiden. In der Praxis bedeutet dies, möglichst viele Komponententests zu vermeiden und Tests auf höherer Ebene / Integration zu bevorzugen, bei denen Implementierungsdetails nicht berücksichtigt werden. Denken Sie daran, dass es kein Patentrezept gibt, die Tests müssen jedoch auf einer bestimmten Ebene an etwas gekoppelt werden. Idealerweise sollte es sich jedoch um eine Schnittstelle handeln, die explizit mit Semantic Versioning versioniert wurde, dh normalerweise auf der veröffentlichten API- / Anwendungsebene (Sie möchten SemVer nicht ausführen) für jede einzelne Einheit in Ihrer Lösung).
quelle
Ihre Tests sind zu eng an die Implementierung gekoppelt und nicht an die Anforderung.
Schreiben Sie Ihre Tests mit Kommentaren wie diesen:
Auf diese Weise können Sie die Bedeutung von Tests nicht umgestalten.
quelle