Ich bin verwirrt darüber, wie man mit TDD richtig arbeitet


8

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


1
Sie müssen keine strenge TDD einhalten, um den größten Nutzen aus Unit-Tests zu ziehen.
Den

2
@Den: "strikte TDD" bedeutet nicht, was das OP glaubt, dass es bedeutet.
Doc Brown

Ich empfehle, vimeo.com/album/3143213/video/71816368 (8LU: Advanced Concepts in TDD) anzusehen. Es könnte Ihnen helfen, sich mit Dingen auseinanderzusetzen.
Andrew Eddie

Antworten:


19

Wenn Sie in TDD theoretisch sind, müssen Sie zuerst Ihren Testanzug schreiben und Ihren Code daraus aufbauen

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,

  • jeweils einen Test schreiben
  • Implementieren Sie nur so viel Code wie nötig, um den Test "grün" zu machen.
  • Refactor (der Code und die Tests)

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.


13

Was mich stolpert, ist, dass ... wenn Sie in TDD theoretisch zuerst Ihren Testanzug schreiben und Ihren Code daraus aufbauen müssen, wie soll ich vorher wissen, dass ich ihn 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.

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.


1
Ihre Antwort hat einige gute Punkte, aber erlauben Sie mir, eine Sache nicht herauszusuchen. „Controller sind in der Regel Integratoren von Komponenten, so dass Sie Integrationstests schreiben, die in der Regel eine Menge von spöttisch erfordern“ - na ja, ich denke , Sie wahrscheinlich gemeint „ wenn Sie zu schreiben versuchen , Unit - Tests für Controller, wird es in der Regel eine Menge von spöttisch erfordern“ . IMHO passt der Begriff "Integrationstest" besser für einen Test ohne Verspottung, bei dem Sie tatsächlich die realen Komponenten und keine Verspottungen verwenden, um zu sehen, ob sie wie beabsichtigt zusammenarbeiten.
Doc Brown

Dank @DocBrown bezog ich mich in der Tat auf einen "Unit-Test, der die Integration / Kommunikation zwischen Komponenten testet", und nicht auf das Konzept von Integrationstests, die die realen Komponenten enthalten.
MichelHenrich

1
Nun, wo wir uns über den Begriff "Integrationstest" einig sind, führt Ihre Antwort meiner Meinung nach direkt zur nächsten Frage: Lohnt es sich wirklich, TDD (oder Unit-Tests) für Controller mit der primären Rolle "Integratoren" zu verwenden? Oder sollte man lieber nur Integrationstests für diese Komponenten schreiben (vielleicht danach)?
Doc Brown

4

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

AccountController controller = new AccountController()

var result = controller.Index(username : "Peter", password: "whatever");

Assert.AreEqual(result.Model.Username, "Peter");

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.


3

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.


2
Ich bin mit dem ersten Teil nicht einverstanden. Tests müssen unabhängig sein. Dies ist bei Unit-Tests eine weitaus höhere Anforderung als bei Code, da die Unabhängigkeit die Wartbarkeit von Unit-Tests verbessert , während die fehlende Wiederverwendung den Produktionscode beeinträchtigt.
Telastyn

1
@ Telastyn-Tests können beim Freigeben von Setup-Code weiterhin unabhängig sein. Sie müssen nur sicherstellen, dass Sie ein neues Fixture verwenden. Dies bedeutet , dass Sie entweder eine gemeinsam genutzte Setup-Methode aufrufen oder implizites Setup verwenden (sofern Ihr Testframework dies unterstützt).
Benjamin Hodgson

1
@BenjaminHodgson - Ich sehe nicht, wie eine gemeinsam genutzte Setup-Methode für einen Test geändert werden kann, ohne einen anderen zu beschädigen.
Telastyn

1
@Telastyn Dies gilt jedoch im Allgemeinen für wiederverwendeten Code. Wenn eine Klasse mehr als einen Client hat, ist es schwieriger, sie zu ändern. Argumentieren Sie für das Kopieren und Einfügen von Duplikaten des Geräte-Setup-Codes in allen Unit-Tests?
Benjamin Hodgson

3
@Telastyn: Wenn das unabhängige Testen von Tests gegen das DRY-Prinzip verstößt, treten beim Versuch, das Design Ihres Codes zu verbessern, unvermeidlich Probleme auf, Sie müssen jedoch 30 Testmethoden mit "ähnlichem Setup" anstelle einer wiederverwendeten Setup-Methode ändern . Das ist eigentlich das Hauptargument, das ich oft gegen TDD höre - zu viel Aufwand, um Tests während des Refactorings zu ändern -, aber es ist fast immer das Problem, dass die Tests nicht trocken genug sind.
Doc Brown

2

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.

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.


0

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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.