Wie erleichtern Komponententests das Design?

43

Unser Kollege wirbt für das Verfassen von Komponententests, weil er uns dabei hilft, unser Design zu verfeinern und Dinge zu überarbeiten, aber ich verstehe nicht, wie. Wenn ich eine CSV-Datei lade und diese analysiere, wie kann ich meinen Entwurf anhand eines Komponententests (der die Werte in den Feldern überprüft) überprüfen? Er erwähnte Kopplung und Modularität usw., aber für mich macht es nicht viel Sinn - aber ich habe nicht viel theoretischen Hintergrund.

Das ist nicht dasselbe wie die Frage, die Sie als Duplikat markiert haben. Ich würde mich für konkrete Beispiele interessieren, wie dies hilft, und nicht nur für die Theorie, dass "es hilft". Ich mag die Antwort unten und den Kommentar, aber ich würde gerne mehr erfahren.

User039402
quelle
7
Mögliches Duplikat von Führt TDD zum guten Design?
gnat
3
Die Antwort unten ist wirklich alles, was Sie wissen müssen. Neben jenen Leuten zu sitzen, die den ganzen Tag über Factory-Factory-Fabriken schreiben, in denen die Abhängigkeit von der Gesamtwurzel injiziert wird, ist ein Typ, der leise einfachen Code gegen Komponententests schreibt, der korrekt funktioniert, leicht zu überprüfen ist und bereits dokumentiert ist.
Robert Harvey
4
@ Gnat Unit-Tests bedeutet nicht automatisch TDD, es ist eine andere Frage
Joppe
11
"Komponententest (Validieren der Werte in den Feldern)" - Sie scheinen Komponententests mit der Eingabevalidierung in Konflikt zu bringen.
Jonrsharpe
1
@jonrsharpe Da es sich um Code handelt, der eine CSV-Datei analysiert, spricht er möglicherweise von einem echten Komponententest, bei dem überprüft wird, ob eine bestimmte CSV-Zeichenfolge die erwartete Ausgabe liefert.
JollyJoker

Antworten:

3

Unit-Tests erleichtern nicht nur das Design, sondern sind auch einer ihrer Hauptvorteile.

Das Schreiben von Test-First vertreibt die Modularität und saubere Codestruktur.

Wenn Sie Ihren Code test-first schreiben, werden Sie feststellen, dass alle "Bedingungen" einer bestimmten Codeeinheit automatisch an Abhängigkeiten weitergegeben werden (normalerweise über Mocks oder Stubs), wenn Sie sie in Ihrem Code annehmen.

"Bei gegebener Bedingung x, erwarte Verhalten y" wird häufig zu einem zu liefernden Stub x(was ein Szenario ist, in dem der Test das Verhalten der aktuellen Komponente verifizieren muss) und ywird zu einem Mock, bei dem ein Aufruf verifiziert wird das Ende des Tests (es sei denn, es ist ein "sollte zurückgeben y", in welchem ​​Fall der Test nur den Rückgabewert explizit verifizieren wird).

Sobald sich dieses Gerät wie angegeben verhält, schreiben Sie die entdeckten Abhängigkeiten (für xund y).

Dies macht das Schreiben von sauberem, modularem Code zu einem sehr einfachen und natürlichen Prozess, bei dem es sonst oft einfach ist, Verantwortlichkeiten zu verwischen und Verhaltensweisen zusammenzufügen, ohne es zu merken.

Wenn Sie später Tests schreiben, erfahren Sie, wenn Ihr Code schlecht strukturiert ist.

Wenn das Schreiben von Tests für einen Codeteil schwierig wird, weil zu viele Dinge zum Stoppen oder Verspotten vorhanden sind oder weil die Dinge zu eng miteinander verbunden sind, wissen Sie, dass Sie Verbesserungen an Ihrem Code vornehmen müssen.

Wenn "Ändern von Tests" zu einer Belastung wird, weil eine Einheit so viele Verhaltensweisen aufweist, wissen Sie, dass Sie Verbesserungen an Ihrem Code vornehmen müssen (oder einfach an Ihrer Vorgehensweise beim Schreiben von Tests - dies ist jedoch nach meiner Erfahrung normalerweise nicht der Fall). .

Wenn Ihre Szenarien zu kompliziert werden ( „wenn xund yund zdann ...“) , weil Sie zu abstrahieren mehr benötigen, können Sie wissen , dass Sie Verbesserungen in Ihrem Code zu machen.

Wenn Sie aufgrund von Duplizierung und Redundanz dieselben Tests in zwei verschiedenen Fixtures durchführen, wissen Sie, dass Sie Verbesserungen an Ihrem Code vornehmen müssen.

Hier ist ein ausgezeichneter Vortrag von Michael Feathers , der die sehr enge Beziehung zwischen Testbarkeit und Design im Code demonstriert (ursprünglich gepostet von displayName in den Kommentaren). Der Vortrag geht auch auf einige häufige Beschwerden und Missverständnisse in Bezug auf gutes Design und Testbarkeit im Allgemeinen ein.

Ameise P
quelle
@SSECommunity: Mit nur 2 positiven Bewertungen ist diese Antwort sehr leicht zu übersehen. Ich kann den in dieser Antwort verlinkten Vortrag von Michael Feathers nur empfehlen.
displayName
103

Das Tolle an Unit-Tests ist, dass Sie Ihren Code so verwenden können, wie andere Programmierer Ihren Code verwenden.

Wenn Ihr Code für den Komponententest umständlich ist, ist die Verwendung wahrscheinlich umständlich. Wenn Sie keine Abhängigkeiten einfügen können, ohne durch Rahmen zu springen, ist die Verwendung Ihres Codes wahrscheinlich unflexibel. Und wenn Sie viel Zeit damit verbringen müssen, Daten einzurichten oder herauszufinden, in welcher Reihenfolge die Dinge zu tun sind, hat der zu testende Code wahrscheinlich zu viel Kopplung und es wird schwierig, mit ihm zu arbeiten.

Telastyn
quelle
7
Gute Antwort. Ich denke immer gerne an meine Tests als den ersten Client des Codes. Wenn es schmerzhaft ist, die Tests zu schreiben, wird es schmerzhaft sein, Code zu schreiben, der die API verbraucht, oder was auch immer ich entwickle.
Stephen Byrne
41
Nach meiner Erfahrung der meisten Unit - Tests nicht „verwenden Sie den Code wie andere Programmierer den Code verwenden.“. Sie verwenden Ihren Code, während Unit-Tests den Code verwenden. Es ist wahr, sie werden viele schwerwiegende Mängel aufdecken. Eine API, die für Unit-Tests entwickelt wurde, ist jedoch möglicherweise nicht die API, die für den allgemeinen Gebrauch am besten geeignet ist. Vereinfacht geschriebene Komponententests erfordern häufig, dass der zugrunde liegende Code zu viele interne Elemente enthält. Auch hier, basierend auf meiner Erfahrung - würde mich interessieren, wie Sie damit umgegangen sind. (Siehe meine Antwort unten)
user949300
7
@ user949300 - Ich bin kein großer Anhänger des ersten Tests. Meine Antwort basiert zunächst auf der Idee des Codes (und sicherlich des Designs). APIs sollten nicht für Unit-Tests entwickelt werden, sondern für Ihren Kunden. Unit-Tests helfen dabei, Ihre Kunden zu schätzen, aber sie sind ein Werkzeug. Sie sind da, um Ihnen zu dienen, nicht umgekehrt. Und sie werden dich mit Sicherheit nicht davon abhalten, beschissenen Code zu schreiben.
Telastyn
3
Meiner Erfahrung nach ist das größte Problem bei Unit-Tests, dass das Schreiben von guten genauso schwierig ist wie das Schreiben von gutem Code. Wenn Sie guten Code nicht von schlechtem Code unterscheiden können, führt das Schreiben von Komponententests nicht zu einer Verbesserung Ihres Codes. Wenn Sie den Komponententest schreiben, müssen Sie in der Lage sein, zwischen einer reibungslosen, angenehmen Verwendung und einer "unangenehmen" oder schwierigen Verwendung zu unterscheiden. Sie können dazu führen, dass Sie Ihren Code ein wenig benutzen, aber sie zwingen Sie nicht, zu erkennen, dass Ihre Handlungen schlecht sind.
jpmc26
2
@ user949300 - das klassische Beispiel, an das ich hier gedacht habe, ist ein Repository, das einen connString benötigt. Angenommen, Sie stellen diese Eigenschaft als öffentlich beschreibbare Eigenschaft zur Verfügung und müssen sie festlegen, nachdem Sie ein neues () Repository erstellt haben. Die Idee ist, dass Sie nach dem fünften oder sechsten Mal einen Test geschrieben haben, der diesen Schritt vergisst - und dadurch abstürzt -, dass Sie "von Natur aus" dazu neigen, connString zu einer Klasseninvarianten zu zwingen -, die im Konstruktor übergeben wird - und damit Ihren Test erstellen Verbessern Sie die API und erhöhen Sie die Wahrscheinlichkeit, dass Produktionscode geschrieben werden kann, der diese Falle vermeidet. Es ist keine Garantie, aber es hilft, imo.
Stephen Byrne
31

Es hat eine ganze Weile gedauert, bis mir klar wurde, aber der eigentliche Vorteil (redigieren: für mich kann Ihre Laufleistung variieren) einer testgetriebenen Entwicklung (unter Verwendung von Komponententests ) besteht darin, dass Sie das API-Design von vornherein durchführen müssen !

Ein typischer Ansatz für die Entwicklung besteht darin, zunächst herauszufinden, wie ein bestimmtes Problem gelöst werden kann, und mit diesem Wissen und dem anfänglichen Implementierungsdesign eine Möglichkeit zu finden, Ihre Lösung aufzurufen. Dies kann zu interessanten Ergebnissen führen.

Wenn TDD tun haben Sie als allererstes den Code schreiben, wird verwenden Ihre Lösung. Eingabeparameter und erwartete Ausgabe, damit Sie sicherstellen können, dass sie richtig ist. Das wiederum erfordert, dass Sie herausfinden, was Sie tatsächlich tun müssen, damit Sie aussagekräftige Tests erstellen können. Dann und nur dann implementieren Sie die Lösung. Es ist auch meine Erfahrung, dass, wenn Sie genau wissen, was Ihr Code erreichen soll, es klarer wird.

Anschließend können Sie nach der Implementierung anhand von Komponententests sicherstellen, dass das Refactoring die Funktionalität nicht beeinträchtigt, und eine Dokumentation zur Verwendung Ihres Codes bereitstellen (von der Sie wissen, dass sie zum Zeitpunkt des bestandenen Tests zutrifft!). Dies ist jedoch zweitrangig - der größte Vorteil ist die Denkweise bei der Erstellung des Codes.

Thorbjørn Ravn Andersen
quelle
Das ist sicherlich ein Vorteil, aber ich denke nicht, dass es der "echte" Vorteil ist - der echte Vorteil ergibt sich aus der Tatsache, dass das Schreiben von Tests für Ihren Code "Bedingungen" auf natürliche Weise auf Abhängigkeiten überträgt und die Überdosierung von Abhängigkeiten unterdrückt (was die Abstraktion weiter fördert) ) bevor es losgeht.
Ant P
Das Problem ist, dass Sie eine ganze Reihe von Tests im Voraus schreiben, die mit dieser API übereinstimmen. Dann funktioniert es nicht genau nach Bedarf und Sie müssen Ihren Code und alle Tests neu schreiben. Für öffentlich zugängliche APIs ist es wahrscheinlich, dass sie sich nicht ändern, und dieser Ansatz ist in Ordnung. Die APIs für Code, die nur intern verwendet werden, ändern sich jedoch erheblich, wenn Sie herausfinden, wie eine Funktion implementiert wird, die viele halbprivate APIs benötigt
Juan Mendes,
@AntP Ja, dies ist Teil des API-Designs.
Thorbjørn Ravn Andersen
@JuanMendes Dies ist nicht ungewöhnlich und diese Tests müssen geändert werden, genau wie jeder andere Code, wenn Sie Anforderungen ändern. Eine gute IDE hilft Ihnen dabei, die Klassen im Rahmen der automatisch ausgeführten Arbeit neu zu fokussieren, wenn Sie die Methodensignaturen usw. ändern.
Thorbjørn Ravn Andersen
@JuanMendes Wenn Sie gute Tests und kleine Einheiten schreiben, ist die Auswirkung des beschriebenen Effekts in der Praxis gering bis gar nicht.
Ant P
6

Ich würde zu 100% zustimmen, dass Unit-Tests helfen, "unser Design zu verfeinern und Dinge zu überarbeiten".

Ich bin mir nicht sicher, ob sie Ihnen beim ersten Entwurf helfen . Ja, sie weisen offensichtliche Mängel auf und zwingen Sie zu der Überlegung, wie der Code testbar gemacht werden kann. Dies sollte zu weniger Nebenwirkungen, einer einfacheren Konfiguration und Einrichtung usw. führen.

Meiner Erfahrung nach führen zu stark vereinfachte Komponententests, die geschrieben wurden, bevor Sie wirklich verstanden haben, wie das Design aussehen soll (das ist zwar eine Übertreibung von Hardcore-TDD, aber zu oft schreiben Codierer einen Test, bevor sie viel denken), häufig zu Anämie Domain-Modelle, die zu viele Interna offenlegen.

Meine Erfahrung mit TDD war vor einigen Jahren, daher bin ich daran interessiert zu hören, welche neueren Techniken beim Schreiben von Tests hilfreich sein könnten, die das zugrunde liegende Design nicht zu sehr beeinflussen. Vielen Dank.

user949300
quelle
Eine große Anzahl von Methodenparametern ist ein Codegeruch und ein Konstruktionsfehler.
Sufian
5

Mit Unit-Tests können Sie sehen, wie die Schnittstellen zwischen Funktionen funktionieren, und erhalten häufig einen Einblick in die Verbesserung des lokalen Designs und des Gesamtdesigns. Wenn Sie Ihre Komponententests während der Codeentwicklung entwickeln, verfügen Sie außerdem über eine fertige Regressionstestsuite. Es spielt keine Rolle, ob Sie eine Benutzeroberfläche oder eine Backend-Bibliothek entwickeln.

Sobald das Programm entwickelt wurde (mit Komponententests), können Sie Tests hinzufügen, um zu bestätigen, dass die Fehler behoben wurden.

Ich benutze TDD für einige meiner Projekte. Ich habe viel Mühe in die Erstellung von Beispielen gesteckt, die ich aus Lehrbüchern oder aus als korrekt angesehenen Papieren gezogen habe, und den Code, den ich entwickle, anhand dieses Beispiels getestet. Alle Missverständnisse, die ich bezüglich der Methoden habe, werden sehr offensichtlich.

Ich neige dazu, ein bisschen lockerer zu sein als einige meiner Kollegen, da es mir egal ist, ob der Code zuerst geschrieben oder der Test zuerst geschrieben wird.

Robert Baron
quelle
Das ist eine großartige Antwort für mich. Würde es Ihnen etwas ausmachen, ein paar Beispiele zu nennen, z. B. eines für jeden Fall (wenn Sie einen Einblick in das Design usw. erhalten).
User039402
5

Wenn Sie einen Komponententest für den Parser durchführen möchten, bei dem der Wert ordnungsgemäß begrenzt wird, können Sie ihn in einer Zeile aus einer CSV-Datei übergeben. Um Ihren Test direkt und kurz zu machen, können Sie ihn mit einer Methode testen, die eine Zeile akzeptiert.

Dadurch trennen Sie automatisch das Lesen von Zeilen vom Lesen einzelner Werte.

Auf einer anderen Ebene möchten Sie möglicherweise nicht alle Arten von physischen CSV-Dateien in Ihr Testprojekt einfügen, sondern etwas Lesbareres tun, indem Sie einfach eine große CSV-Zeichenfolge in Ihrem Test deklarieren, um die Lesbarkeit und die Absicht des Tests zu verbessern. Dies führt dazu, dass Sie Ihren Parser von allen E / A-Vorgängen entkoppeln, die Sie an anderer Stelle ausführen würden.

Nur ein einfaches Beispiel, fang einfach an zu üben, du wirst die Magie irgendwann spüren (ich habe).

Joppe
quelle
4

Vereinfacht ausgedrückt hilft das Schreiben von Komponententests dabei, Fehler in Ihrem Code aufzudecken.

Diese spektakuläre Anleitung zum Schreiben von testbarem Code , geschrieben von Jonathan Wolter, Russ Ruffer und Miško Hevery, enthält zahlreiche Beispiele dafür, wie Fehler im Code, die das Testen verhindern, auch die einfache Wiederverwendung und Flexibilität desselben Codes verhindern. Wenn Ihr Code also testbar ist, ist er einfacher zu verwenden. Die meisten "Moralvorstellungen" sind lächerlich einfache Tipps, die das Code-Design erheblich verbessern ( Dependency Injection FTW).

Beispiel: Es ist sehr schwierig zu testen, ob die Methode computeStuff ordnungsgemäß funktioniert, wenn der Cache anfängt, Daten zu entfernen. Dies liegt daran, dass Sie manuell Mist zum Cache hinzufügen müssen, bis der "BigCache" fast voll ist.

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Wenn wir jedoch die Abhängigkeitsinjektion verwenden, ist es viel einfacher zu testen, ob die Methode computeStuff ordnungsgemäß funktioniert, wenn der Cache beginnt, Daten zu entfernen. Alles, was wir tun, ist, einen Test zu erstellen, in dem wir new HereIUseDI(buildSmallCache()); Notice aufrufen . Wir haben eine differenziertere Kontrolle über das Objekt und es zahlt sich sofort aus.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Ähnliche Vorteile ergeben sich, wenn unser Code Daten erfordert, die normalerweise in einer Datenbank gespeichert sind. Geben Sie einfach GENAU die Daten weiter, die Sie benötigen.

Ivan
quelle
2
Ehrlich gesagt bin ich mir nicht sicher, wie Sie das Beispiel meinen. In welcher Beziehung steht die Methode computeStuff zum Cache?
John V
1
@ user970696 - Ja, ich gehe davon aus, dass "computeStuff ()" den Cache verwendet. Die Frage lautet: "Funktioniert computeStuff () die ganze Zeit korrekt (abhängig vom Cache-Status)?" Infolgedessen ist es schwierig zu bestätigen, dass computeStuff () für ALLE MÖGLICHEN CACHE-STAATEN das tut, was Sie möchten, wenn Sie / nicht direkt festlegen können. Erstellen Sie den Cache, weil Sie die Zeile "cacheOfExpensiveComputation = buildBigCache ();" (im Gegensatz zur Weitergabe des Cache direkt über den Konstruktor)
Ivan
0

Je nachdem , was von ‚Unit Tests‘ zu verstehen ist, glaube ich nicht wirklich Low-Level - Einheit Tests erleichtern gutes Design so viel wie etwas höheres Niveau Integrationstests - Tests , dass Test , dass eine Gruppe von Akteuren (Klassen, Funktionen, was auch immer) in Ihr Code kann ordnungsgemäß kombiniert werden, um eine ganze Reihe wünschenswerter Verhaltensweisen zu erzielen, die zwischen dem Entwicklerteam und dem Produktbesitzer vereinbart wurden.

Wenn Sie Tests auf diesen Ebenen schreiben können, werden Sie dazu gebracht, netten, logischen, API-ähnlichen Code zu erstellen, der nicht viele verrückte Abhängigkeiten erfordert. Der Wunsch nach einer einfachen Testkonfiguration wird Sie natürlich dazu bringen, nicht viele zu haben verrückte Abhängigkeiten oder eng gekoppelter Code.

Machen Sie aber keinen Fehler - Unit-Tests können sowohl zu schlechtem als auch zu gutem Design führen. Ich habe gesehen, wie Entwickler ein Stück Code, das bereits ein ansprechendes logisches Design und ein einzelnes Anliegen hat, auseinander genommen und mehr Schnittstellen lediglich zum Testen eingeführt haben, wodurch der Code weniger lesbar und schwerer zu ändern ist Wenn der Entwickler entschieden hat, dass viele Unit-Tests auf niedriger Ebene keine Tests auf höherer Ebene erfordern, hat er möglicherweise sogar noch mehr Bugs. Ein besonderes Lieblingsbeispiel ist ein Fehler, den ich behoben habe, bei dem es viele sehr kaputte, 'testbare' Codes gab, die sich auf das Abrufen von Informationen in und aus der Zwischenablage bezogen. Alles aufgeschlüsselt und entkoppelt bis auf kleinste Detailebenen, mit vielen Schnittstellen, vielen Verspottungen in den Tests und anderen lustigen Dingen. Einziges Problem: Es gab keinen Code, der tatsächlich mit dem Mechanismus der Zwischenablage des Betriebssystems interagierte.

Unit-Tests können Ihr Design definitiv vorantreiben, führen Sie jedoch nicht automatisch zu einem guten Design. Sie müssen Ideen darüber haben, was gutes Design ist, die über "Dieser Code ist getestet, daher ist er prüfbar, daher ist er gut" hinausgehen.

Natürlich sind einige dieser Warnungen möglicherweise nicht so relevant, wenn Sie zu den Personen gehören, für die "Unit-Tests" "automatische Tests" bedeuten, die nicht über die Benutzeroberfläche ausgeführt werden Integrationstests sind häufig die nützlichsten, wenn es darum geht, Ihr Design voranzutreiben.

topo Setzen Sie Monica wieder ein
quelle
-2

Unit-Tests können beim Refactoring helfen, wenn der neue Code alle alten Tests besteht.

Angenommen, Sie haben einen Bubblesort implementiert, weil Sie es eilig hatten und sich nicht um die Leistung Gedanken gemacht haben, aber jetzt möchten Sie einen QuickSort, da die Daten länger werden. Wenn alle Tests bestanden sind, sieht es gut aus.

Natürlich müssen die Tests umfassend sein, damit dies funktioniert. In meinem Beispiel decken Ihre Tests möglicherweise nicht die Stabilität ab, da es sich nicht um Bubblesort handelte.

om
quelle
1
Dies ist wahr, aber es ist eher ein Vorteil für die Wartbarkeit als ein direkter Einfluss auf die Qualität des Code-Designs.
Ant P
@AntP, fragte das OP nach Refactoring und Unit-Tests.
om
1
Die Frage bezog sich auf das Refactoring, aber die eigentliche Frage war, wie Unit-Tests das Code-Design verbessern / verifizieren könnten - nicht den Prozess des Refactorings selbst vereinfachen.
Ant P
-3

Ich habe Unit-Tests als äußerst wertvoll erachtet, um die längerfristige Wartung eines Projekts zu erleichtern. Wenn ich nach Monaten zu einem Projekt zurückkehre und mich nicht an viele Details erinnere, hindert mich das Ausführen von Tests daran, Dinge zu beschädigen.

David Baker
quelle
6
Das ist sicherlich ein wichtiger Aspekt von Tests, beantwortet aber nicht wirklich die Frage (weshalb Tests nicht gut sind, sondern wie sie das Design beeinflussen).
Hulk