Ich versuche zu verstehen, was die Idee hinter TDD ist und wie ein Team damit arbeiten soll. Ich habe den folgenden Testfall mit NUnit + Moq (nur durch Speicher schreiben, es ist nicht sicher, dass das Beispiel kompiliert wird, aber es sollte erklärend sein):
[Test]
public void WhenUserLogsCorrectlyIsRedirectedToLoginCorrectView() {
Mock<IUserDatabaseRepository> repoMock = new Mock<IUserDatabaseRepository>();
repoMock.Setup(m => m.GetUser(It.IsAny())).Returns(new User { Name = "Peter" });
Mock<ILoginHelper> loginHelperMock = new Mock<ILoginHelper>();
loginHelperMock.Setup(m => m.Login(It.IsAny(), It.IsAny())).Returns(true);
Mock<IViewModelFactory> factoryMock = new Mock<IViewModelFactory>();
factoryMock.Setup(m => m.CreateViewModel()).Returns(new LoginViewModel());
AccountController controller = new AccountController(repoMock.Object, loginHelperMock.Object, factoryMock.Object)
var result = controller.Index(username : "Peter", password: "whatever");
Assert.AreEqual(result.Model.Username, "Peter");
}
AccountController hat 3 Abhängigkeiten, die ich verspotte, damit ich bei der Orchestrierung im Controller überprüfen kann, ob eine Anmeldung korrekt war oder nicht.
Was mich stolpert, ist, dass ... wenn Sie in TDD theoretisch zuerst Ihre Testsuite schreiben und Ihren Code daraus aufbauen müssen, wie soll ich vorher wissen, dass ich sie verwenden muss, um meine Operation auszuführen diese drei Abhängigkeiten und dass die Operation bestimmte Operationen aufruft? Es ist, als müsste ich die Innereien des zu testenden Subjekts kennen, bevor ich es überhaupt implementiere, um die Abhängigkeiten zu verspotten und die Klasse zu isolieren und eine Art Schreibtest zu erstellen - Code schreiben - Test bei Bedarf zu ändern.
Ohne die Innereien meines Codes zu kennen und nur den Test auszudrücken, könnte ich ihn natürlich so ausdrücken, als würde er nur den ILoginHelper benötigen und "magisch" annehmen, bevor er den Code schreibt, dass er den Benutzer bei einer erfolgreichen Anmeldung zurückgibt (und letztendlich Beachten Sie, dass das zugrunde liegende Framework nicht so funktioniert, z. B. nur eine ID anstelle des vollständigen Objekts zurückgibt.
Verstehe ich TDD falsch? Welches ist eine typische TDD-Praxis in einem komplexen Fall?
Vielen Dank
Antworten:
Hier ist dein Missverständnis. Bei TDD geht es nicht darum, zuerst eine vollständige Testsuite zu schreiben - das ist ein falscher Mythos. TDD bedeutet, in kleinen Zyklen zu arbeiten,
Die Erstellung einer Testsuite erfolgt also nicht in einem Schritt und nicht "bevor der Code geschrieben wird", sondern mit der Implementierung des betreffenden Codes.
Auf Ihr Beispiel angewendet: Sie sollten versuchen, mit einem einfachen Test für einen Controller ohne Abhängigkeiten zu beginnen (so etwas wie ein Prototyp). Dann implementieren Sie den Controller und den Refactor. Anschließend fügen Sie entweder einen neuen Test hinzu, der von Ihrem Controller etwas mehr erwartet, oder Sie überarbeiten / erweitern Ihren vorhandenen Test. Dann ändern Sie Ihren Controller, bis der neue Test "grün" wird. Auf diese Weise beginnen Sie mit einer einfachen Kombination aus Tests und Testobjekt und erhalten einen komplexen Test und Testobjekt.
Wenn Sie diesen Weg gehen, werden Sie zu einem bestimmten Zeitpunkt herausfinden, welche zusätzlichen Daten Sie als Eingabe benötigen, damit der Controller seine Arbeit erledigen kann. Dies kann in der Tat zu einem Zeitpunkt geschehen, an dem Sie versuchen, eine Controller-Methode zu implementieren, und nicht beim Entwerfen des nächsten Tests. Dies ist der Punkt, an dem Sie die Implementierung der Methode für kurze Zeit beenden und zuerst die fehlenden Abhängigkeiten einführen (möglicherweise durch ein Refactoring des Konstruktors Ihres Controllers). Dies führt direkt zu einem Refactoring Ihrer vorhandenen Tests. In TDD ändern Sie normalerweise zuerst die Tests, die den Konstruktor aufrufen, und fügen anschließend die neuen Konstruktorattribute hinzu. Und hier verwickeln sich Codierung und Schreiben der Tests vollständig.
quelle
Es fühlt sich falsch an, oder? Und das sollte es auch - nicht, weil Ihre Tests für den Controller in irgendeiner Weise falsch oder "schlecht" sind, sondern weil Sie den Controller testen möchten, bevor Sie überhaupt etwas zu "steuern" haben. :) :)
Mein Punkt ist: TDD wird sich für Sie natürlicher anfühlen, wenn Sie es auf der Ebene der "Geschäftsregeln" und der "echten Anwendungslogik" beginnen. Dies ist auch dort, wo es am nützlichsten ist. Controller befassen sich normalerweise nur mit der Delegierung an andere Komponenten. Um zu testen, ob die Delegierung korrekt ausgeführt wird, müssen Sie natürlich wissen, an welches Objekt sie delegieren soll. Das einzige Problem ist, wenn Sie versuchen, dies zu tun, bevor Sie eine echte Logik implementiert haben. Mein Vorschlag ist, dass Sie versuchen, den LoginHelper zu implementieren, indem Sie TDD beispielsweise "verhaltensorientierter" ausführen. Es wird sich natürlicher anfühlen und Sie werden wahrscheinlich mehr von seinen Vorteilen sehen.
Um eine allgemeinere Antwort zu erhalten: TDD ist eine Praxis, mit der wir Tests erstellen, bevor wir den Code schreiben, den wir benötigen, aber nicht spezifiziert, welche Art von Tests. Controller sind normalerweise Integratoren von Komponenten, daher schreiben Sie Komponententests, die normalerweise viel Verspottung erfordern. Wenn Sie die Anwendungslogik schreiben (Geschäftsregeln, z. B. Bestellung aufgeben, Benutzerauthentifizierungen validieren usw.), schreiben Sie Verhaltenstests, bei denen es sich normalerweise um zustandsbasierte Tests handelt (gegebene Eingabe vs. gewünschte Ausgabe). Dieser Unterschied wird von der TDD-Community oft als Mockism vs. Statism bezeichnet. Ich gehöre zu der (kleinen) Gruppe, die darauf besteht, dass beide Wege korrekt sind. Sie bieten lediglich unterschiedliche Kompromisse und sind daher für verschiedene Szenarien wie oben beschrieben nützlich.
quelle
Während TDD eine Test-First-Methode ist, müssen Sie nicht viel Zeit damit verbringen, Testcode zu schreiben, bevor Sie Produktionscode schreiben.
In diesem Beispiel besteht die in Kent Becks wegweisendem Buch über TDD ( 1 ) beschriebene Idee von TDD darin, mit etwas wirklich Einfachem zu beginnen, wie vielleicht
Zuerst wissen Sie nicht alles, was Sie brauchen, um die Arbeit zu erledigen. Sie wissen nur, dass Sie einen Controller mit einer Indexmethode benötigen, die Ihnen ein Modell mit einem Benutzernamen gibt. Sie wissen noch nicht, wie es das machen wird. Sie haben sich gerade ein Ziel gesetzt.
Dann funktioniert das mit allen verfügbaren Mitteln, möglicherweise nur mit dem richtigen Hardcodieren des richtigen Ergebnisses. In nachfolgenden Refactorings (und sogar durch Hinzufügen zusätzlicher Tests) fügen Sie Schritt für Schritt mehr Raffinesse hinzu. Mit TDD können Sie so kleine Schritte machen, wie Sie brauchen, um vorwärts zu kommen, aber Sie können auch so große Schritte machen, wie es Ihre Fähigkeiten und Kenntnisse erlauben. Wenn Sie einen kurzen Zyklus zwischen Testcode und Produktionscode machen, erhalten Sie Feedback zu jedem kleinen Schritt und wissen nahezu sofort, ob das, was Sie gerade getan haben, funktioniert hat und ob es andere Probleme verursacht hat, die zuvor funktioniert haben.
Robert Martin in ( 2 ) plädiert auch für eine sehr kurze Zykluszeit zwischen dem Schreiben von Testcode und dem Schreiben von Produktionscode.
quelle
Möglicherweise benötigen Sie all diese Komplexität für einen konzeptionell einfachen Komponententest, aber Sie werden den Test mit ziemlicher Sicherheit gar nicht erst so schreiben.
Zunächst sollte die komplexe Einrichtung in Ihren ersten sechs Zeilen in einen eigenständigen, wiederverwendbaren Gerätecode zerlegt werden. Die Prinzipien der wartbaren Programmierung gelten für Testcode genauso wie für Geschäftscode. Wenn Sie dasselbe Gerät für zwei oder mehr Tests verwenden, sollte es auf jeden Fall in eine separate Methode umgestaltet werden, damit Sie nur eine Ablenkungslinie in Ihrem Test haben, oder in den Klassen-Setup-Code, damit Sie keine haben.
Aber das Wichtigste: Das Schreiben eines Tests garantiert nicht, dass er für immer unverändert bleibt . Wenn Sie die Mitarbeiter eines Methodenaufrufs nicht kennen, können Sie sie mit ziemlicher Sicherheit beim ersten Versuch nicht richtig erraten. Es ist nichts Falsches daran, Ihren Testcode zusammen mit Ihrem Geschäftscode umzugestalten, wenn sich die öffentliche API ändert. Es ist wahr, dass das Ziel von TDD darin besteht, die richtige, verwendbare API zu schreiben, aber dies wird kaum jemals zu 100% erreicht. Anforderungen immerNachträglich ändern, und allzu oft erfordert dies unbedingt Mitarbeiter, die es beim Schreiben der ersten Iteration einer Geschichte nicht gab. In diesem Fall bleibt nichts anderes zu tun, als in die Kugel zu beißen und vorhandene Tests zusammen mit Ihrer Anwendung zu ändern. In diesen Fällen gelangt der größte Teil des von Ihnen angegebenen Einrichtungscodes in Ihre Testsuite.
quelle
Ja, bis zu einem gewissen Grad. Ich glaube nicht, dass Sie falsch verstehen, wie TDD funktioniert.
Das Problem ist, dass es sich - wie andere bereits erwähnt haben - zunächst sehr seltsam anfühlt, fast falsch, dies so zu tun. Meiner Meinung nach zeigt dies tatsächlich, was meiner Meinung nach der größte Vorteil von TDD ist: Sie müssen die Anforderungen richtig verstehen, bevor Sie Code schreiben.
Als Programmierer schreiben wir gerne Code. Was sich für uns also "richtig" und "natürlich" anfühlt, ist es, Anforderungen zu überfliegen und so schnell wie möglich festzuhalten. Die Entwurfsprobleme werden dann allmählich sichtbar, wenn Sie die Codebasis aufbauen und testen. Sie überarbeiten und reparieren sie, und die Dinge verbessern sich allmählich und bewegen sich in Richtung Ihres Ziels.
Obwohl es Spaß macht, ist dies keine besonders effiziente Art, Dinge zu tun. Es ist weitaus besser, zuerst zu verstehen, was ein Softwaremodul tun sollte, Ihre Tests zu notieren und dann den Code zu schreiben. Es ist weniger Refactoring, weniger Testwartung und zwingt Sie zu einer besseren Architektur aus dem Block.
Ich mache nicht viel TDD und ich halte das Mantra "100% Code Coverage" für sinnlos. Besonders in Fällen wie Ihrem. Die Einführung von TDD ist jedoch immer noch von großem Wert, da es ein Segen ist, sicherzustellen, dass die Dinge in Ihrem Code gut gestaltet und gepflegt sind.
Zusammenfassend ist die Tatsache, dass Sie dieses Bizarre finden, wahrscheinlich ein gutes Zeichen dafür, dass Sie auf dem richtigen Weg sind.
quelle
Das Verspotten von Daten ist nur die Praxis der Verwendung von Dummy-Daten. Die Moq-Frameworks erleichtern das Erstellen von Dummy-Daten.
ARRANGE | ACT | BEHAUPTEN
Bei TDD geht es im Allgemeinen darum, Ihre Tests zu erstellen und diese Tests dann als "bestanden" zu validieren. Anfänglich schlägt der erste Test fehl, da der Code zur Validierung dieses Tests noch nicht erstellt wurde. Ich glaube, dies ist tatsächlich eine bestimmte Art von Test. "Rot / Grün" -Tests, von denen ich sicher bin, dass sie heute die Quelle für "Test Driven" -Methoden sind.
Im Allgemeinen validieren die Tests die kleinen logischen Nuggets, mit denen der Code für größere Bilder funktioniert. Sie könnten auf der kleinsten Funktionsebene beginnen und sich dann zu den komplizierteren Funktionen hocharbeiten.
Ja, manchmal ist das Setup oder "Verspotten" etwas intensiv, weshalb die Verwendung eines MoQ-Frameworks eine gute Idee ist. Wenn Sie sich jedoch auf die Kerngeschäftslogik konzentrieren, führen Ihre Tests zu einer vorteilhaften Sicherheit, dass es funktioniert wie erwartet und beabsichtigt.
Persönlich teste ich meine Controller nicht, da alles, was der Controller verwendet, auf Funktion getestet wurde und wir das Framework im Allgemeinen nicht testen müssen.
quelle