Ich habe kürzlich "All the Little Things" von RailsConf 2014 gesehen. Während dieses Vortrags überarbeitet Sandi Metz eine Funktion, die eine große verschachtelte if-Anweisung enthält:
def tick
if @name != 'Aged Brie' && @name != 'Backstage passes to a TAFKAL80ETC concert'
if @quality > 0
if @name != 'Sulfuras, Hand of Ragnaros'
@quality -= 1
end
end
else
...
end
...
end
Der erste Schritt besteht darin, die Funktion in mehrere kleinere zu unterteilen:
def tick
case name
when 'Aged Brie'
return brie_tick
...
end
end
def brie_tick
@days_remaining -= 1
return if quality >= 50
@quality += 1
@quality += 1 if @days_remaining <= 0
end
Was ich interessant fand, war die Art und Weise, wie diese kleineren Funktionen geschrieben wurden. brie_tick
Zum Beispiel wurde nicht durch Extrahieren der relevanten Teile der ursprünglichen tick
Funktion geschrieben, sondern von Grund auf unter Bezugnahme auf die test_brie_*
Komponententests. Nachdem alle diese Unit-Tests bestanden waren, brie_tick
wurde dies als erledigt angesehen. Nachdem alle kleinen Funktionen ausgeführt wurden, wurde die ursprüngliche monolithische tick
Funktion gelöscht.
Leider schien der Moderator nicht zu wissen, dass dieser Ansatz dazu führte, dass drei der vier *_tick
Funktionen falsch waren (und die andere leer war!). Es gibt Randfälle, in denen sich das Verhalten der *_tick
Funktionen von dem der ursprünglichen tick
Funktion unterscheidet. Zum Beispiel @days_remaining <= 0
in brie_tick
sollte < 0
- so brie_tick
funktioniert nicht richtig , wenn sie aufgerufen mit days_remaining == 1
und quality < 50
.
Was ist hier falsch gelaufen? Ist dies ein Testfehler - weil es für diese speziellen Randfälle keine Tests gab? Oder ein Fehler beim Refactoring - weil der Code Schritt für Schritt hätte transformiert und nicht von Grund auf neu geschrieben werden müssen?
quelle
Antworten:
Beide. Das Refactoring, bei dem nur die Standardschritte aus Fowlers Originalbuch verwendet werden, ist definitiv weniger fehleranfällig als das Umschreiben. Daher ist es häufig vorzuziehen, nur diese Art von Babyschritten zu verwenden. Selbst wenn es keine Komponententests für jeden Randfall gibt und die Umgebung keine automatischen Refactorings bereitstellt, hat eine einzelne Codeänderung wie "Erklärungsvariable einführen" oder "Extraktionsfunktion" eine viel geringere Chance, die Verhaltensdetails des zu ändern vorhandener Code als ein vollständiges Umschreiben einer Funktion.
Manchmal ist es jedoch genau das, was Sie tun müssen oder möchten, wenn Sie einen Codeabschnitt neu schreiben. Und wenn das der Fall ist, brauchen Sie bessere Tests.
Beachten Sie, dass selbst bei Verwendung eines Refactoring-Tools immer ein gewisses Risiko besteht, dass beim Ändern von Code Fehler auftreten, unabhängig davon, ob kleinere oder größere Schritte angewendet werden. Deshalb braucht Refactoring immer Tests. Beachten Sie auch, dass Tests nur die Wahrscheinlichkeit von Fehlern verringern können, aber niemals deren Fehlen beweisen. Die Verwendung von Techniken wie dem Betrachten des Codes und der Zweigabdeckung kann Ihnen jedoch ein hohes Konfidenzniveau geben, und im Falle eines Umschreibens eines Codeabschnitts ist dies der Fall oft wert, solche Techniken anzuwenden.
quelle
brie_tick
während der problematische@days_remaining == 1
Fall immer noch nie getestet wird, indem beispielsweise mit@days_remaining
set to10
und getestet wird-10
.Eines der Dinge, die bei der Arbeit mit Legacy-Code wirklich schwierig sind: ein umfassendes Verständnis des aktuellen Verhaltens zu erlangen.
Legacy-Code ohne Tests, die alle Verhaltensweisen einschränken, ist ein weit verbreitetes Muster in freier Wildbahn. Was Sie mit einer Vermutung zurücklässt: Bedeutet das, dass die uneingeschränkten Verhaltensweisen freie Variablen sind? oder Anforderungen, die nicht spezifiziert sind?
Aus dem Vortrag :
Dies ist der konservativere Ansatz; Wenn die Anforderungen möglicherweise nicht genau festgelegt sind und die Tests nicht die gesamte vorhandene Logik erfassen, müssen Sie sehr vorsichtig sein, wie Sie vorgehen.
Mit Sicherheit können Sie behaupten, dass Sie einen "Testfehler" haben, wenn die Tests das Verhalten des Systems nicht ausreichend beschreiben. Und ich denke, das ist fair - aber nicht wirklich nützlich; Dies ist ein häufiges Problem in freier Wildbahn.
Das Problem ist nicht ganz, dass die Transformationen Schritt für Schritt hätten erfolgen sollen. Vielmehr stimmte die Wahl des Refactoring-Tools (menschlicher Tastaturbediener statt geführter Automatisierung) aufgrund der höheren Fehlerrate nicht gut mit der Testabdeckung überein.
Dies hätte entweder durch die Verwendung von Refactoring-Tools mit höherer Zuverlässigkeit oder durch die Einführung einer größeren Anzahl von Tests zur Verbesserung der Systembeschränkungen behoben werden können.
Ich denke, Ihre Konjunktion ist schlecht gewählt.
AND
nichtOR
.quelle
Refactoring sollte das von außen sichtbare Verhalten Ihres Codes nicht ändern. Das ist das Ziel.
Wenn Ihre Komponententests fehlschlagen, bedeutet dies, dass Sie das Verhalten geändert haben. Das Bestehen von Unit-Tests ist jedoch nie das Ziel. Es hilft mehr oder weniger, Ihr Ziel zu erreichen. Wenn das Refactoring das von außen sichtbare Verhalten ändert und alle Komponententests bestanden wurden, ist Ihr Refactoring fehlgeschlagen.
Arbeitseinheitentests geben in diesem Fall nur das falsche Erfolgsgefühl. Aber was ist schief gelaufen? Zwei Dinge: Das Refactoring war nachlässig und die Unit-Tests waren nicht sehr gut.
quelle
Wenn Sie "richtig" als "Test bestanden" definieren, ist es per Definition nicht falsch, nicht getestetes Verhalten zu ändern.
Wenn eine bestimmte Kante Verhalten sollte definiert, fügen Sie einen Test für ihn, wenn nicht, dann ist es OK , um es egal , was passiert. Wenn Sie wirklich pedantisch sind, können Sie einen Test schreiben, der überprüft,
true
wann in diesem Randfall, um zu dokumentieren, dass es Ihnen egal ist, wie sich das Verhalten verhält.quelle