Wie halten Sie Ihre Komponententests beim Refactoring aufrecht?

29

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).

Alex Feinman
quelle
7
Ich hätte gedacht, dass bei TDD der erste Schritt bei der Umgestaltung darin besteht, einen Test zu schreiben, der fehlschlägt, und dann den Code umzugestalten, damit er funktioniert.
Matt Ellen
Kann Ihre IDE nicht herausfinden, wie Sie die Tests auch umgestalten können?
@ Thorbjørn Ravn Andersen, ja, und ich habe eine neue Frage geschrieben, die mich fragte, was ich fragen wollte (aber diese als interessante Variante behielt ich bei; siehe Azheglovs Antwort, die im Wesentlichen das ist, was Sie sagen)
Alex Feinman,
Ziehen Sie in Betracht, dieser Frage thar Info hinzuzufügen?

Antworten:

35

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.

Azheglov
quelle
7
Gleicher Code X kopiert 15 Stellen. An jedem Ort angepasst. Sie machen es zu einer gemeinsamen Bibliothek und parametrisieren das X oder verwenden ein Strategiemuster, um diese Unterschiede zu berücksichtigen. Ich garantiere, dass die Komponententests für X fehlschlagen werden. Clients von X schlagen fehl, da sich die öffentliche Schnittstelle geringfügig ändert. Redesign oder Refactor? Ich nenne es Refactor, aber so oder so bricht es alle möglichen Dinge. Das Fazit ist, Sie können nicht umgestalten, wenn Sie nicht genau wissen, wie alles zusammenpasst. Dann ist das Reparieren der Tests mühsam, aber letztendlich trivial.
Kevin
3
Wenn die Tests ständig angepasst werden müssen, deutet dies wahrscheinlich auf zu detaillierte Tests hin. Angenommen, ein Codeteil muss unter bestimmten Umständen Ereignisse A, B und C in einer nicht bestimmten Reihenfolge auslösen. Der alte Code führt dies in der Reihenfolge ABC aus und Tests erwarten die Ereignisse in dieser Reihenfolge. Wenn der überarbeitete Code Ereignisse in der Reihenfolge ACB ausspuckt, funktioniert er weiterhin gemäß der Spezifikation, der Test schlägt jedoch fehl.
Otto
3
@ Kevin: Ich glaube, was Sie beschreiben, ist ein Redesign, weil sich die öffentliche Oberfläche ändert. Fowlers Definition von Refactoring ("die interne Struktur des Codes ändern, ohne sein äußeres Verhalten zu ändern") ist darüber ziemlich klar.
Azheglov
3
@azheglov: Vielleicht, aber meiner Erfahrung nach, wenn die Implementierung schlecht ist, ist es auch die Benutzeroberfläche
Kevin
2
Eine zutreffende und klare Frage endet in einer Diskussion über die Bedeutung des Wortes. Wen kümmert es, wie Sie es nennen, lassen Sie uns diese Diskussion woanders haben. In der Zwischenzeit lässt diese Antwort keine wirkliche Antwort aus, hat aber bei weitem immer noch die meisten positiven Stimmen. Ich verstehe, warum die Leute TDD als Religion bezeichnen.
Dirk Boer
21

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.

Tim Murphy
quelle
7

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:

  • Verwenden Sie nach Möglichkeit Stubs anstelle von Mocks. Weitere Informationen finden Sie in Fabio Perieras Blog über tautologische Tests und in meinem Blog über tautologische Tests .
  • Wenn Sie Mocks verwenden, sollten Sie die Reihenfolge der aufgerufenen Methoden nicht überprüfen, es sei denn, dies ist wichtig.
  • Vermeiden Sie es, den internen Status Ihres SUT zu überprüfen. Verwenden Sie nach Möglichkeit dessen externe API.
  • Vermeiden Sie testspezifische Logik im Produktionscode
  • Vermeiden Sie es, testspezifische Unterklassen zu verwenden.

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:

Wenn Sie einen Mockist-Test schreiben, testen Sie die ausgehenden Anrufe des SUT, um sicherzustellen, dass es ordnungsgemäß mit seinen Lieferanten spricht. Bei einem klassischen Test geht es nur um den Endzustand - nicht darum, wie dieser abgeleitet wurde. Mockist-Tests sind daher eher an die Implementierung einer Methode gekoppelt. Wenn Sie die Art der Anrufe an Mitarbeiter ändern, wird in der Regel ein Spottest abgebrochen.

[...]

Die Kopplung an die Implementierung wirkt sich auch störend auf das Refactoring aus, da bei Änderungen an der Implementierung die Wahrscheinlichkeit größer ist, dass Tests abgebrochen werden als bei klassischen Tests.

Fowler - Mocks sind keine Stubs

Perfektionist
quelle
Fowler schrieb buchstäblich das Buch über Refactoring; und das maßgeblichste Buch über Unit-Tests (xUnit Test Patterns von Gerard Meszaros) befindet sich in Fowlers "Signature" -Serie. Wenn er also sagt, dass das Refactoring einen Test brechen kann, hat er wahrscheinlich Recht.
Perfektionist
5

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!

Frank Shearar
quelle
Was ist, wenn er nur den Namen einer durch die Tests getesteten Methode ändert? Die Tests schlagen fehl, wenn Sie sie nicht auch in den Tests umbenennen. Hier ändert er das Verhalten des Programms nicht.
Oscar Mederos
2
In diesem Fall werden auch seine Tests überarbeitet. Sie müssen jedoch vorsichtig sein: Zuerst benennen Sie die Methode um, dann führen Sie Ihren Test aus. Es sollte aus den richtigen Gründen fehlschlagen (es kann nicht kompiliert werden (C #), es wird eine MessageNotUnderstood-Ausnahme (Smalltalk) ausgegeben, es scheint nichts zu passieren (Null-Ess-Muster von Objective-C)). Dann ändern Sie Ihren Test, da Sie wissen, dass Sie nicht versehentlich einen Fehler verursacht haben. "Wenn Ihre Tests brechen" bedeutet "wenn Ihre Tests brechen, nachdem Sie das Refactoring abgeschlossen haben", mit anderen Worten. Versuchen Sie, die Kleingeldstücke klein zu halten!
Frank Shearar
1
Unit Tests sind von Natur aus an die Struktur des Codes gekoppelt. Zum Beispiel hat Fowler viele in refactoring.com/catalog , die Unit-Tests beeinflussen würden (z. B. Methode ausblenden, Inline-Methode, Fehlercode mit Ausnahme ersetzen usw.).
Kristian H
falsch. Das Zusammenführen von zwei Methoden ist offensichtlich ein Refactoring, das offizielle Namen hat (z. B. passt das Inline-Refactoring der Methode zur Definition), und es bricht die Tests einer inline-Methode ab - einige der Testfälle sollten jetzt mit anderen Mitteln umgeschrieben / getestet werden. Ich muss das Verhalten eines Programms nicht ändern, um Komponententests zu unterbrechen. Alles, was ich tun muss, ist, Interna umzustrukturieren, mit denen Komponententests gekoppelt sind. Solange sich das Verhalten eines Programms nicht ändert, entspricht dies immer noch der Definition von Refactoring.
KolA
Ich habe das oben Gesagte unter der Annahme gut geschriebener Tests geschrieben: Wenn Sie Ihre Implementierung testen - wenn die Struktur des Tests die Interna des getesteten Codes widerspiegelt, ist dies sicher. Testen Sie in diesem Fall den Vertrag der Einheit und nicht die Implementierung.
Frank Shearar
4

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:

>  Keep the API the same, but change how the API is implemented internally
>  Change the API

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:

int DoSomethingInterestingAPI_NEW( int takes_more_arguments );
int DoSomethingInterestingAPI_OLD();
int DoSomethingInterestingAPI() { DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API);

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.

Lewis Pringle
quelle
Diese Antwort gefällt mir, weil es offensichtlich ist, dass Unit Tests für das SUT mit externen Clients für eine veröffentlichte API identisch sind. Was Sie verschreiben, ist dem SemVer-Protokoll zum Verwalten der veröffentlichten Bibliothek / Komponente sehr ähnlich, um die Abhängigkeitshölle zu vermeiden. Dies kostet jedoch Zeit und Flexibilität, und die Extrapolation dieses Ansatzes auf die öffentliche Schnittstelle jeder Mikroeinheit bedeutet auch die Extrapolation der Kosten. Ein flexiblerer Ansatz besteht darin, Tests so weit wie möglich von der Implementierung zu entkoppeln, z. B. Integrationstests oder ein separates DSL, um
Testein-
1

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.

gbjbaanb
quelle
1
Die nützlichste Antwort, die sich tatsächlich mit der Frage befasst: Bauen Sie Ihre Testabdeckung nicht auf einer wackeligen Grundlage interner Trivia auf oder erwarten Sie, dass sie ständig auseinanderfällt. Am schlechtesten bewertet sie jedoch, weil TDD genau das Gegenteil vorschreibt. Dies ist, was Sie bekommen, um auf die unbequeme Wahrheit über einen überhypten Ansatz hinzuweisen.
KolA
1

Halten Sie die Testsuite während und nach dem Refactoring mit der Codebasis synchron

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).

KolA
quelle
0

Ihre Tests sind zu eng an die Implementierung gekoppelt und nicht an die Anforderung.

Schreiben Sie Ihre Tests mit Kommentaren wie diesen:

//given something
...test code...
//and something else
...test code...
//when something happens
...test code...
//then the state should be...
...test code...

Auf diese Weise können Sie die Bedeutung von Tests nicht umgestalten.

mcintyre321
quelle