Kurze Einführung in diese Frage. Ich benutze jetzt TDD und in letzter Zeit BDD seit über einem Jahr. Ich benutze Techniken wie das Verspotten, um meine Tests effizienter zu schreiben. In letzter Zeit habe ich ein persönliches Projekt gestartet, um ein kleines Money-Management-Programm für mich zu schreiben. Da ich keinen alten Code hatte, war es das perfekte Projekt, um mit TDD zu beginnen. Leider habe ich die Freude an TDD nicht so sehr erlebt. Es hat sogar meinen Spaß so sehr verdorben, dass ich das Projekt aufgegeben habe.
Was war das Problem? Nun, ich habe den TDD-ähnlichen Ansatz verwendet, um die Tests / Anforderungen das Design des Programms weiterentwickeln zu lassen. Das Problem war, dass mehr als die Hälfte der Entwicklungszeit für das Schreiben / Refactor-Tests. Am Ende wollte ich keine Features mehr implementieren, da ich viele Tests überarbeiten und schreiben musste.
Bei der Arbeit habe ich viel Legacy-Code. Hier schreibe ich immer mehr Integrations- und Akzeptanztests und weniger Unit-Tests. Dies scheint kein schlechter Ansatz zu sein, da Fehler meist durch Akzeptanz- und Integrationstests erkannt werden.
Meine Idee war, dass ich am Ende mehr Integrations- und Akzeptanztests schreiben könnte als Unit-Tests. Wie ich bereits sagte, sind die Komponententests zur Erkennung von Fehlern nicht besser als die Integrations- / Abnahmetests. Unit-Test sind auch gut für das Design. Da ich viele von ihnen geschrieben habe, sind meine Klassen immer so gestaltet, dass sie gut testbar sind. Darüber hinaus führt der Ansatz, dass die Tests / Anforderungen das Design leiten, in den meisten Fällen zu einem besseren Design. Der letzte Vorteil von Unit-Tests ist, dass sie schneller sind. Ich habe genug Integrationstests geschrieben, um zu wissen, dass sie fast so schnell sein können wie die Unit-Tests.
Nachdem ich das Web durchgesehen hatte, stellte ich fest, dass es sehr ähnliche Ideen zu meinen gibt, die hier und da erwähnt werden . Was haltet ihr von dieser Idee?
Bearbeiten
Als ich auf die Fragen eines Beispiels antwortete, in dem das Design gut war, brauchte ich ein großes Refactoring für die nächste Anforderung:
Zuerst gab es einige Anforderungen, um bestimmte Befehle auszuführen. Ich habe einen erweiterbaren Befehlsparser geschrieben, der Befehle von einer Eingabeaufforderung aus analysierte und den richtigen Befehl für das Modell aufrief. Das Ergebnis wurde in einer Ansichtsmodellklasse dargestellt:
Hier war nichts falsch. Alle Klassen waren unabhängig voneinander und ich konnte leicht neue Befehle hinzufügen, neue Daten anzeigen.
Die nächste Anforderung war, dass jeder Befehl eine eigene Ansichtsdarstellung haben sollte - eine Art Vorschau des Befehlsergebnisses. Ich habe das Programm überarbeitet, um ein besseres Design für die neue Anforderung zu erzielen:
Das war auch gut so, denn jetzt hat jeder Befehl ein eigenes Ansichtsmodell und damit eine eigene Vorschau.
Die Sache ist, dass der Befehlsparser geändert wurde, um ein Token-basiertes Parsen der Befehle zu verwenden, und seine Fähigkeit, die Befehle auszuführen, wurde gestrippt. Jeder Befehl hat ein eigenes Ansichtsmodell, und das Datenansichtsmodell kennt nur das aktuelle Befehlsansichtsmodell, das dann die anzuzeigenden Daten kennt.
Ich wollte an dieser Stelle nur wissen, ob das neue Design keine bestehenden Anforderungen erfüllt. Ich musste keinen meiner Abnahmetests ändern. Ich musste fast JEDEN Komponententest überarbeiten oder löschen, was einen riesigen Haufen Arbeit darstellte.
Was ich hier zeigen wollte, ist eine häufige Situation, die während der Entwicklung häufig vorkam. Es gab kein Problem mit dem alten oder dem neuen Design, sie änderten sich einfach auf natürliche Weise mit den Anforderungen - wie ich es verstanden habe, ist dies ein Vorteil von TDD, dass sich das Design weiterentwickelt.
Fazit
Vielen Dank für alle Antworten und Diskussionen. Als Zusammenfassung dieser Diskussion habe ich mir einen Ansatz überlegt, den ich bei meinem nächsten Projekt testen werde.
- Zunächst schreibe ich alle Tests, bevor ich etwas implementiere, wie ich es immer getan habe.
- Für Anforderungen schreibe ich zunächst einige Abnahmetests, die das gesamte Programm testen. Dann schreibe ich einige Integrationstests für die Komponenten, in denen ich die Anforderung implementieren muss. Wenn es eine Komponente gibt, die eng mit einer anderen Komponente zusammenarbeitet, um diese Anforderung umzusetzen, würde ich auch einige Integrationstests schreiben, bei denen beide Komponenten zusammen getestet werden. Last but not least würde ich Unit-Tests für diese bestimmte Klasse schreiben, wenn ich einen Algorithmus oder eine andere Klasse mit hoher Permutation schreiben müsste, z. B. einen Serializer. Alle anderen Klassen werden nicht getestet, es werden jedoch Unit-Tests durchgeführt.
- Bei Fehlern kann der Prozess vereinfacht werden. Normalerweise wird ein Fehler durch eine oder zwei Komponenten verursacht. In diesem Fall würde ich einen Integrationstest für die Komponenten schreiben, der den Fehler testet. Wenn es sich um einen Algorithmus handeln würde, würde ich nur einen Komponententest schreiben. Wenn es nicht einfach ist, die Komponente zu erkennen, in der der Fehler auftritt, würde ich einen Akzeptanztest schreiben, um den Fehler zu lokalisieren - dies sollte eine Ausnahme sein.