Soweit ich das verstehe, scheinen sich die meisten Leute einig zu sein, dass private Methoden nicht direkt getestet werden sollten, sondern mit welchen öffentlichen Methoden auch immer. Ich kann ihren Punkt sehen, aber ich habe einige Probleme damit, wenn ich versuche, den "Drei Gesetzen von TDD" zu folgen und den "Rot-Grün-Refaktor" -Zyklus zu verwenden. Ich denke, das lässt sich am besten anhand eines Beispiels erklären:
Im Moment benötige ich ein Programm, das eine Datei (mit durch Tabulatoren getrennten Daten) lesen und alle Spalten herausfiltern kann, die nicht numerische Daten enthalten. Vermutlich gibt es bereits einige einfache Tools, um dies zu tun, aber ich habe mich dazu entschlossen, es von Grund auf neu zu implementieren, hauptsächlich, weil ich dachte, dass es ein schönes und sauberes Projekt für mich sein könnte, etwas Übung mit TDD zu bekommen.
Also setze ich zuerst den roten Hut auf, das heißt, ich brauche einen Test, der fehlschlägt. Ich dachte, ich brauche eine Methode, die alle nicht numerischen Felder in einer Zeile findet. Also schreibe ich einen einfachen Test, der natürlich nicht sofort kompiliert werden kann, also beginne ich, die Funktion selbst zu schreiben, und nach ein paar Zyklen hin und her (rot / grün) habe ich eine funktionierende Funktion und einen vollständigen Test.
Als nächstes fahre ich mit der Funktion "gatherNonNumericColumns" fort, die die Datei zeilenweise liest und in jeder Zeile meine Funktion "findNonNumericFields" aufruft, um alle Spalten zu sammeln, die schließlich entfernt werden müssen. Ein paar Rot-Grün-Zyklen, und ich habe wieder eine funktionierende Funktion und einen vollständigen Test.
Jetzt denke ich, ich sollte umgestalten. Da meine Methode "findNonNumericFields" nur deswegen entworfen wurde, weil ich dachte, sie würde für die Implementierung von "gatherNonNumericColumns" benötigt, erscheint es mir sinnvoll, "findNonNumericFields" privat werden zu lassen. Dies würde jedoch meine ersten Tests unterbrechen, da sie keinen Zugriff mehr auf die von ihnen getestete Methode hätten.
Also habe ich eine private Methode und eine Reihe von Tests, die es testen. Da so viele Leute raten, dass private Methoden nicht getestet werden sollten, habe ich das Gefühl, dass ich mich hier in eine Ecke gestrichen habe. Aber wo genau bin ich gescheitert?
Ich könnte auf einer höheren Ebene anfangen und einen Test schreiben, der testet, welche Methode irgendwann zu meiner öffentlichen Methode wird (findAndFilterOutAllNonNumericalColumns), der sich jedoch etwas gegen den ganzen Punkt von TDD anfühlt (zumindest laut Uncle Bob). : Dass Sie ständig zwischen dem Schreiben von Tests und Produktionscode wechseln sollten und dass alle Ihre Tests zu jedem Zeitpunkt in der letzten Minute oder so funktionierten. Denn wenn ich mit dem Schreiben eines Tests für eine öffentliche Methode beginne, wird es einige Minuten (oder Stunden oder sogar Tage in sehr komplexen Fällen) dauern, bis ich alle Details in den privaten Methoden zum Funktionieren gebracht habe, damit der Test die Öffentlichkeit testet Methode besteht.
Also, was ist zu tun? Ist TDD (mit dem schnellen Rot-Grün-Refaktor-Zyklus) einfach nicht mit privaten Methoden kompatibel? Oder liegt ein Fehler in meinem Design vor?
quelle
private
ob es Sinn macht, es zu tun.Antworten:
Einheiten
Ich glaube, ich kann genau feststellen, wo das Problem angefangen hat:
Darauf sollte sich sofort die Frage stellen, ob es sich um eine separate testbare Einheit
gatherNonNumericColumns
oder einen Teil derselben handelt.Wenn die Antwort " Ja, getrennt " lautet , ist Ihre Vorgehensweise einfach: Diese Methode muss für eine entsprechende Klasse öffentlich sein, damit sie als Einheit getestet werden kann. Ihre Mentalität ist so etwas wie "Ich muss eine Methode testen und ich muss auch eine andere Methode testen"
Nach allem, was Sie sagen, haben Sie herausgefunden, dass die Antwort " Nein, Teil desselben " ist. Zu diesem Zeitpunkt sollte Ihr Plan nicht mehr darin bestehen, vollständig zu schreiben und zu testen und
findNonNumericFields
dann zu schreibengatherNonNumericColumns
. Stattdessen sollte es einfach zu schreiben seingatherNonNumericColumns
. VorerstfindNonNumericFields
sollte dies nur ein Teil des Ziels sein, an das Sie denken, wenn Sie Ihren nächsten roten Testfall auswählen und das Refactoring durchführen. Diesmal lautet Ihre Einstellung "Ich muss eine Methode testen und dabei sollte ich bedenken, dass meine fertige Implementierung wahrscheinlich diese andere Methode enthalten wird".Einen kurzen Zyklus halten
Das oben Genannte sollte nicht zu den Problemen führen, die Sie in Ihrem vorletzten Absatz beschreiben:
Zu keinem Zeitpunkt müssen Sie für diese Technik einen roten Test schreiben, der nur dann grün wird, wenn Sie die gesamte
findNonNumericFields
Neuentwicklung durchführen. Viel wahrscheinlicher ist,findNonNumericFields
dass ein Code in der öffentlichen Methode, die Sie testen, als Inline-Code gestartet wird, der im Verlauf mehrerer Zyklen aufgebaut und schließlich während eines Refactorings extrahiert wird.Fahrplan
Um eine ungefähre Roadmap für dieses Beispiel anzugeben: Ich kenne nicht die genauen Testfälle, die Sie verwendet haben, aber Sie haben
gatherNonNumericColumns
als öffentliche Methode geschrieben. Dann stimmen die Testfälle höchstwahrscheinlich mit denen überein, für die Sie geschrieben haben. DabeifindNonNumericFields
wird jeweils eine Tabelle mit nur einer Zeile verwendet. Wenn dieses einzeilige Szenario vollständig implementiert wurde und Sie einen Test schreiben wollten, um Sie zu zwingen, die Methode zu extrahieren, würden Sie einen zweizeiligen Fall schreiben, bei dem Sie Ihre Iteration hinzufügen müssten.quelle
Viele Leute denken, dass Unit-Tests methodenbasiert sind. es ist nicht. Es sollte sich um die kleinste Einheit handeln, die Sinn macht. Für die meisten Dinge bedeutet dies, dass die Klasse genau das ist, was Sie als Ganzes testen sollten. Keine individuellen Methoden drauf.
Jetzt werden Sie natürlich Methoden für die Klasse aufrufen, aber Sie sollten sich die Tests als auf das Black-Box-Objekt zutreffend vorstellen, das Sie haben, damit Sie sehen können, welche logischen Operationen Ihre Klasse bereitstellt. Dies sind die Dinge, die Sie testen müssen. Wenn Ihre Klasse so groß ist, dass die logische Operation zu komplex ist, liegt ein Entwurfsproblem vor, das zuerst behoben werden sollte.
Eine Klasse mit tausend Methoden mag testbar erscheinen, aber wenn Sie jede Methode nur einzeln testen, testen Sie die Klasse nicht wirklich. Einige Klassen müssen sich möglicherweise in einem bestimmten Status befinden, bevor eine Methode aufgerufen wird, z. B. eine Netzwerkklasse, für die vor dem Senden von Daten eine Verbindung eingerichtet werden muss. Die Sendedatenmethode kann nicht unabhängig von der gesamten Klasse betrachtet werden.
Sie sollten also sehen, dass private Methoden für das Testen irrelevant sind. Wenn Sie Ihre privaten Methoden nicht ausführen können, indem Sie die öffentliche Schnittstelle Ihrer Klasse aufrufen, sind diese privaten Methoden unbrauchbar und werden ohnehin nicht verwendet.
Ich denke, viele Leute versuchen, private Methoden in testbare Einheiten umzuwandeln, weil es einfach erscheint, Tests für sie durchzuführen, aber dies geht zu weit mit der Testgranularität. Martin Fowler sagt
Das ist für ein objektorientiertes System sehr sinnvoll, da Objekte als Einheiten konzipiert sind. Wenn Sie einzelne Methoden testen möchten, sollten Sie möglicherweise ein prozedurales System wie C erstellen oder stattdessen eine Klasse, die ausschließlich aus statischen Funktionen besteht.
quelle
Die Tatsache, dass Ihre Datenerfassungsmethoden komplex genug sind, um Tests zu verdienen, und sich von Ihrem Hauptziel trennen, um eigene Methoden zu sein, anstatt Teil einiger Schleifen zu sein, weist auf die Lösung hin: Machen Sie diese Methoden nicht privat, sondern zu Mitgliedern einer anderen Klasse das bietet Sammeln / Filtern / Tabellieren Funktionalität.
Dann schreiben Sie an einer Stelle Tests für die dummen Datenmungsaspekte der Helferklasse (z. B. "Unterscheiden von Zahlen von Zeichen") und an einer anderen Stelle Tests für Ihr primäres Ziel (z. B. "Abrufen der Verkaufszahlen"), und Sie ziehen an Sie müssen die grundlegenden Filtertests in den Tests für Ihre normale Geschäftslogik nicht wiederholen.
Ganz allgemein, wenn Ihre Klasse, die eine Sache ausführt, umfangreichen Code enthält, um eine andere Sache zu tun, die für ihren primären Zweck erforderlich ist, aber von diesem getrennt ist, sollte dieser Code in einer anderen Klasse leben und über öffentliche Methoden aufgerufen werden. Es sollte nicht in privaten Ecken einer Klasse versteckt werden, die diesen Code nur aus Versehen enthält. Dies verbessert gleichzeitig die Testbarkeit und Verständlichkeit.
quelle
Persönlich bin ich der Meinung, dass Sie bei der Erstellung der Tests zu weit in die Implementierung eingedrungen sind. Sie gingen davon aus, dass Sie bestimmte Methoden benötigen würden. Aber brauchen Sie sie wirklich, um das zu tun, was die Klasse tun soll? Würde die Klasse scheitern, wenn jemand mitkam und sie intern überarbeitete? Wenn Sie wurden mit der Klasse (und das sollte die Einstellung des Testers meiner Meinung nach ), könnte man wirklich weniger egal , wenn es eine explizite Methode für Zahlen zu überprüfen ist.
Sie sollten die öffentliche Schnittstelle einer Klasse testen. Die private Implementierung ist aus einem bestimmten Grund privat. Es ist nicht Teil der öffentlichen Schnittstelle, da es nicht benötigt wird und sich ändern kann. Es ist ein Implementierungsdetail.
Wenn Sie Tests für die öffentliche Schnittstelle schreiben, wird das Problem, auf das Sie gestoßen sind, nie tatsächlich auftreten. Entweder können Sie Testfälle für die öffentliche Schnittstelle erstellen, die Ihre privaten Methoden abdecken (großartig), oder Sie können nicht. In diesem Fall ist es möglicherweise an der Zeit, sich eingehende Gedanken über die privaten Methoden zu machen und sie möglicherweise zu vernichten, wenn sie ohnehin nicht erreichbar sind.
quelle
Sie führen TDD nicht basierend auf den erwarteten internen Aufgaben der Klasse durch.
Ihre Testfälle sollten darauf basieren, was die Klasse / Funktionalität / das Programm mit der Außenwelt zu tun hat. In Ihrem Beispiel wird der Benutzer Ihre Leserklasse jemals mit to aufrufen
find all the non-numerical fields in a line?
Wenn die Antwort "nein" ist, ist es ein schlechter Test, an erster Stelle zu schreiben. Sie möchten den Test zur Funktionalität auf Klassen- / Schnittstellenebene schreiben - nicht auf die Ebene "Was muss die Klassenmethode implementieren, damit dies funktioniert? ", Wie Ihr Test lautet.
Der TDD-Fluss ist:
Es ist NICHT zu tun, "weil ich X in Zukunft als private Methode benötigen werde, lass es mich implementieren und teste es zuerst." Wenn Sie dies bemerken, machen Sie die "rote" Phase falsch. Dies scheint hier Ihr Problem zu sein.
Wenn Sie häufig Tests für Methoden schreiben, die zu privaten Methoden werden, führen Sie eine der folgenden Aktionen aus:
quelle
Sie stoßen beim Testen im Allgemeinen auf ein weit verbreitetes Missverständnis.
Die meisten Testanfänger denken zunächst so:
und so weiter.
Das Problem hierbei ist, dass Sie tatsächlich keinen Einheitentest für die Funktion H haben. Der Test, der H testen soll, testet tatsächlich gleichzeitig H, G und F.
Um dies zu lösen, muss man sich darüber im Klaren sein, dass testbare Einheiten niemals voneinander abhängen dürfen, sondern von ihren Schnittstellen . In Ihrem Fall, in dem die Einheiten einfache Funktionen sind, sind die Schnittstellen nur ihre Rufsignatur. Sie müssen daher G so implementieren, dass es mit jeder Funktion mit der gleichen Signatur wie F verwendet werden kann.
Wie genau dies gemacht werden kann, hängt von Ihrer Programmiersprache ab. In vielen Sprachen können Sie Funktionen (oder Zeiger darauf) als Argumente an andere Funktionen übergeben. Auf diese Weise können Sie jede Funktion einzeln testen.
quelle
Die Tests, die Sie während der testgesteuerten Entwicklung schreiben, sollen sicherstellen, dass eine Klasse ihre öffentliche API korrekt implementiert, und gleichzeitig sicherstellen, dass diese öffentliche API einfach zu testen und zu verwenden ist.
Sie können diese API auf jeden Fall mit privaten Methoden implementieren. Es ist jedoch nicht erforderlich, Tests über TDD zu erstellen. Die Funktionalität wird getestet, da die öffentliche API ordnungsgemäß funktioniert.
Nehmen wir nun an, Ihre privaten Methoden sind so kompliziert, dass sie eigenständige Tests verdienen - aber als Teil der öffentlichen API Ihrer ursprünglichen Klasse ergeben sie keinen Sinn. Nun, dies bedeutet wahrscheinlich, dass es sich tatsächlich um öffentliche Methoden für eine andere Klasse handeln sollte - eine, die Ihre ursprüngliche Klasse in ihrer eigenen Implementierung ausnutzt.
Indem Sie nur die öffentliche API testen, können Sie die Implementierungsdetails künftig viel einfacher ändern. Nicht hilfreiche Tests ärgern Sie später nur, wenn sie neu geschrieben werden müssen, um einige elegante Umgestaltungen zu unterstützen, die Sie gerade entdeckt haben.
quelle
Ich denke, die richtige Antwort ist die Schlussfolgerung, zu der Sie gekommen sind, als Sie mit den öffentlichen Methoden angefangen haben. Sie würden damit beginnen, einen Test zu schreiben, der diese Methode aufruft. Es würde fehlschlagen, also erstellen Sie eine Methode mit diesem Namen, die nichts tut. Dann korrigieren Sie vielleicht einen Test, der nach einem Rückgabewert sucht.
(Ich weiß nicht genau, was Ihre Funktion tut. Gibt sie eine Zeichenfolge mit dem Dateiinhalt zurück, bei der die nicht numerischen Werte entfernt wurden?)
Wenn Ihre Methode eine Zeichenfolge zurückgibt, suchen Sie nach diesem Rückgabewert. Sie bauen es also einfach weiter auf.
Ich denke, alles, was in einer privaten Methode passiert, sollte zu einem bestimmten Zeitpunkt während Ihres Prozesses in der öffentlichen Methode sein und dann nur als Teil eines Umgestaltungsschritts in die private Methode verschoben werden. Refactoring erfordert meines Wissens keine fehlgeschlagenen Tests. Sie müssen nur fehlerhafte Tests durchführen, wenn Sie Funktionen hinzufügen. Sie müssen Ihre Tests nur nach dem Refactor ausführen, um sicherzustellen, dass alle bestanden werden.
quelle
Es gibt ein altes Sprichwort.
Die Leute scheinen zu glauben, dass man sich beim TDD einfach hinsetzt, Tests schreibt und das Design einfach magisch abläuft. Das stimmt nicht. Sie benötigen einen übergeordneten Plan. Ich habe festgestellt, dass TDD meine besten Ergebnisse liefert, wenn ich zuerst die Schnittstelle (öffentliche API) entwerfe. Persönlich erstelle ich ein Ist
interface
, das die Klasse zuerst definiert.keuchen Ich schrieb etwas "Code", bevor ich irgendwelche Tests schrieb! Nun, nein. Habe ich nicht. Ich habe einen Vertrag geschrieben , der befolgt werden muss, ein Design . Ich vermute, Sie könnten ähnliche Ergebnisse erzielen, wenn Sie ein UML-Diagramm auf Millimeterpapier notieren. Der Punkt ist, Sie müssen einen Plan haben. TDD ist keine Lizenz, um ein Stück Code zu hacken.
Ich habe wirklich das Gefühl, dass "Test First" eine falsche Bezeichnung ist. Erst entwerfen, dann testen.
Befolgen Sie natürlich die Ratschläge, die andere zum Extrahieren weiterer Klassen aus Ihrem Code gegeben haben. Wenn Sie das Bedürfnis haben, die Interna einer Klasse zu testen, extrahieren Sie diese Interna in eine einfach zu testende Einheit und injizieren Sie sie.
quelle
Denken Sie daran, dass Tests auch überarbeitet werden können! Wenn Sie eine Methode privat machen, reduzieren Sie die öffentliche API, und daher ist es durchaus akzeptabel, einige entsprechende Tests für diese "verlorene Funktionalität" (AKA-reduzierte Komplexität) wegzuwerfen.
Andere haben angegeben, dass Ihre private Methode entweder als Teil Ihrer anderen API-Tests aufgerufen wird oder nicht erreichbar und daher löschbar ist. In der Tat sind die Dinge feinkörniger, wenn wir über Ausführungspfade nachdenken .
Wenn wir zum Beispiel eine öffentliche Methode haben, die eine Division durchführt, möchten wir vielleicht den Pfad testen, der eine Division durch Null ergibt. Wenn wir die Methode privat machen, haben wir die Wahl: Entweder können wir den Pfad der Division durch Null berücksichtigen, oder wir können diesen Pfad eliminieren, indem wir überlegen, wie er von den anderen Methoden aufgerufen wird.
Auf diese Weise können wir einige Tests wegwerfen (z. B. durch Null teilen) und die anderen in Bezug auf die verbleibende öffentliche API umgestalten. In einer idealen Welt kümmern sich die vorhandenen Tests natürlich um alle verbleibenden Pfade, aber die Realität ist immer ein Kompromiss;)
quelle
Es gibt Zeiten, in denen eine private Methode zu einer öffentlichen Methode einer anderen Klasse gemacht werden kann.
Beispielsweise verfügen Sie möglicherweise über private Methoden, die nicht threadsicher sind und die Klasse in einem temporären Zustand belassen. Diese Methoden können in eine separate Klasse verschoben werden, die von Ihrer ersten Klasse privat verwaltet wird. Wenn es sich bei Ihrer Klasse also um eine Warteschlange handelt, können Sie eine InternalQueue-Klasse mit öffentlichen Methoden haben, und die Queue-Klasse enthält die InternalQueue-Instanz privat. Auf diese Weise können Sie die interne Warteschlange testen und die einzelnen Vorgänge in der InternalQueue ermitteln.
(Dies ist am offensichtlichsten, wenn Sie sich vorstellen, dass es keine List-Klasse gibt, und wenn Sie versucht haben, die List-Funktionen als private Methoden in der Klasse zu implementieren, die sie verwendet.)
quelle
Ich frage mich, warum Ihre Sprache nur zwei Ebenen der Privatsphäre hat, vollständig öffentlich und vollständig privat.
Können Sie dafür sorgen, dass Ihre nicht öffentlichen Methoden paketzugänglich sind oder so? Legen Sie dann Ihre Tests in dasselbe Paket und genießen Sie es, das Innenleben zu testen, das nicht Teil der öffentlichen Schnittstelle ist. Ihr Build-System schließt Tests beim Erstellen einer Release-Binärdatei aus.
Natürlich müssen Sie manchmal wirklich private Methoden haben, auf die nur die definierende Klasse zugreifen kann. Ich hoffe, alle diese Methoden sind sehr klein. Im Allgemeinen hilft es viel, die Methoden klein zu halten (z. B. unter 20 Zeilen): Testen, Warten und einfaches Verstehen des Codes werden einfacher.
quelle
Ich habe gelegentlich private Methoden zum Schutz übergangen, um feinkörnigere Tests zu ermöglichen (enger als die offen gelegte öffentliche API). Dies sollte eher die (hoffentlich sehr seltene) Ausnahme als die Regel sein, kann jedoch in bestimmten Fällen hilfreich sein, auf die Sie möglicherweise stoßen. Dies ist auch etwas, worauf Sie beim Erstellen einer öffentlichen API überhaupt nicht achten sollten. Es handelt sich eher um "Betrug", den man in diesen seltenen Situationen für Software zur internen Verwendung verwenden kann.
quelle
Ich habe das erlebt und deinen Schmerz gespürt.
Meine Lösung war:
hören Sie auf, Tests wie den Bau eines Monolithen zu behandeln.
Denken Sie daran, dass Sie, wenn Sie eine Reihe von Tests geschrieben haben, z. B. 5, um einige Funktionen auszuschalten, nicht alle diese Tests beibehalten müssen , insbesondere wenn dies Teil eines anderen Objekts ist.
Zum Beispiel habe ich oft:
also dann habe ich
Wenn ich jedoch jetzt Funktionen höherer Ebenen hinzufüge, die diese Funktion aufrufen und über eine Vielzahl von Tests verfügen, kann ich diese Tests niedrigerer Ebenen möglicherweise auf Folgendes reduzieren:
Der Teufel steckt im Detail und die Fähigkeit, dies zu tun, wird von den Umständen abhängen.
quelle
Dreht sich die Sonne um die Erde oder die Erde um die Sonne? Laut Einstein lautet die Antwort ja oder beides, da sich beide Modelle nur durch den Gesichtspunkt unterscheiden. Ebenso stehen die Verkapselung und die testgetriebene Entwicklung nur in Konflikt, weil wir glauben, dass dies der Fall ist. Wir sitzen hier wie Galileo und der Papst und schleudern Beleidigungen aufeinander: Dummkopf, sehen Sie nicht, dass auch private Methoden getestet werden müssen? Ketzer, brecht die Einkapselung nicht! Ebenso können wir, wenn wir erkennen, dass die Wahrheit größer ist als wir dachten, versuchen, die Tests für die privaten Schnittstellen so zu kapseln, dass die Tests für die öffentlichen Schnittstellen die Kapselung nicht aufbrechen.
Versuchen Sie Folgendes: Fügen Sie zwei Methoden hinzu, eine, die keine Eingabe hat, aber nur die Anzahl der privaten Tests zurückgibt, und eine, die eine Testnummer als Parameter verwendet und bestanden / nicht bestanden zurückgibt.
quelle