Schwierigkeiten mit TDD & Refactoring (oder - warum ist das schmerzhafter als es sein sollte?)

20

Ich wollte mir beibringen, den TDD-Ansatz zu verwenden, und ich hatte ein Projekt, an dem ich schon eine Weile arbeiten wollte. Es war kein großes Projekt, also dachte ich, es wäre ein guter Kandidat für TDD. Ich habe jedoch das Gefühl, dass etwas schief gelaufen ist. Lassen Sie mich ein Beispiel geben:

Auf hoher Ebene ist mein Projekt ein Add-In für Microsoft OneNote, mit dem ich Projekte einfacher nachverfolgen und verwalten kann. Jetzt wollte ich auch die Geschäftslogik dafür so weit wie möglich von OneNote entkoppelt halten, falls ich beschloss, eines Tages meinen eigenen benutzerdefinierten Speicher und ein Back-End zu erstellen.

Zuerst begann ich mit einem Akzeptanztest für einfache Wörter, um zu skizzieren, was meine erste Funktion tun sollte. Es sieht ungefähr so ​​aus (der Kürze halber dumm stellen):

  1. Benutzer klickt auf Projekt erstellen
  2. Benutzer gibt den Titel des Projekts ein
  3. Stellen Sie sicher, dass das Projekt korrekt erstellt wurde

Überspringe die UI-Sachen und mache eine Zwischenplanung. Ich komme zu meinem ersten Unit-Test:

[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
    var testController = new Controller();
    Project newProject = testController(A.Dummy<String>());
    Assert.IsNotNull(newProject);
}

So weit, ist es gut. Rot, Grün, Refactor, etc. Okay, jetzt muss es wirklich etwas sparen. Hier ein paar Schritte rauszuschneiden, macht mich fertig.

[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
    var fakeDataStore = A.Fake<IDataStore>();
    var testController = new Controller(fakeDataStore);
    String expectedTitle = fixture.Create<String>("Title");
    Project newProject = testController(expectedTitle);

    Assert.AreEqual(expectedTitle, newProject.Title);
}

Ich fühle mich zu diesem Zeitpunkt immer noch gut. Ich habe noch keinen konkreten Datenspeicher, aber ich habe die Benutzeroberfläche so erstellt, wie ich es mir vorgestellt habe.

Ich werde hier ein paar Schritte überspringen, weil dieser Beitrag lang genug wird, aber ich habe ähnliche Prozesse befolgt und komme schließlich zu diesem Test für meinen Datenspeicher:

[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
    /* snip init code */
    testDataStore.SaveNewProject(A.Dummy<IProject>());
    A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}

Dies war gut, bis ich versuchte, es umzusetzen:

public String SaveNewProject(IProject project)
{
    Page projectPage = oneNoteInterop.CreatePage(...);
}

Und da ist das Problem, wo das "..." ist. Ich erkenne jetzt an DIESEM Punkt, dass CreatePage eine Abschnitts-ID erfordert. Ich habe das damals nicht gemerkt, als ich auf Controllerebene nachgedacht habe, weil ich mich nur mit dem Testen der für den Controller relevanten Bits befasst habe. Den ganzen Weg hier unten ist mir jedoch klar, dass ich den Benutzer nach einem Speicherort für das Projekt fragen muss. Jetzt muss ich dem Datenspeicher eine Standort-ID hinzufügen, dann eine zum Projekt hinzufügen, dann eine zum Controller hinzufügen und zu ALLEN Tests hinzufügen, die bereits für all diese Dinge geschrieben wurden. Es ist sehr schnell langweilig geworden und ich kann nicht anders, als das Gefühl zu haben, ich hätte es schneller bemerkt, wenn ich das Design im Voraus entworfen hätte, anstatt es während des TDD-Prozesses entwerfen zu lassen.

Kann mir bitte jemand erklären, ob ich etwas falsch gemacht habe? Gibt es überhaupt eine Möglichkeit, diese Art des Refactorings zu vermeiden? Oder ist das üblich? Wenn es üblich ist, gibt es Möglichkeiten, es schmerzloser zu machen?

Vielen Dank an alle!

Landon
quelle
Sie erhalten einige sehr aufschlussreiche Kommentare, wenn Sie dieses Thema in diesem Diskussionsforum veröffentlichen: groups.google.com/forum/#!forum/…, das speziell für TDD-Themen vorgesehen ist.
Chuck Krutsinger
1
Wenn Sie all Ihren Tests etwas hinzufügen müssen, hört es sich so an, als wären Ihre Tests schlecht geschrieben. Sie sollten Ihre Tests überarbeiten und in Betracht ziehen, ein vernünftiges Gerät zu verwenden.
Dave Hillier

Antworten:

19

Auch wenn TDD (zu Recht) als eine Möglichkeit angepriesen wird, Ihre Software zu entwerfen und zu erweitern, ist es dennoch eine gute Idee, im Voraus über Design und Architektur nachzudenken. IMO, "das Design im Voraus skizzieren" ist faires Spiel. Häufig liegt dies jedoch auf einer höheren Ebene als die Entwurfsentscheidungen, zu denen Sie durch TDD geführt werden.

Es stimmt auch, dass Sie bei Änderungen in der Regel Tests aktualisieren müssen. Es gibt keine Möglichkeit, dies vollständig zu beseitigen, aber Sie können einige Maßnahmen ergreifen, um Ihre Tests weniger brüchig zu machen und die Schmerzen zu minimieren.

  1. Halten Sie Implementierungsdetails so weit wie möglich aus Ihren Tests heraus. Dies bedeutet, nur mit öffentlichen Methoden zu testen und wenn möglich zustandsbasiert statt interaktionsbasiert zu überprüfen . Mit anderen Worten, wenn Sie das Ergebnis von etwas testen und nicht die Schritte , um dorthin zu gelangen, sollten Ihre Tests weniger anfällig sein.

  2. Minimieren Sie die Duplizierung in Ihrem Testcode, genau wie Sie es im Produktionscode tun würden. Dieser Beitrag ist eine gute Referenz. In Ihrem Beispiel scheint es schmerzhaft zu sein, die IDEigenschaft zu Ihrem Konstruktor hinzuzufügen , da Sie den Konstruktor in mehreren Tests direkt aufgerufen haben. Versuchen Sie stattdessen, die Erstellung des Objekts in eine Methode zu extrahieren oder sie für jeden Test in einer Testinitialisierungsmethode einmal zu initialisieren.

jhewlett
quelle
Ich habe die Vorzüge von zustandsbasiert und interaktionsbasiert gelesen und die meiste Zeit verstanden. Allerdings sehe ich nicht in jedem Fall, wie es möglich ist, Eigenschaften für den Test AUSDRÜCKLICH offenzulegen. Nimm mein Beispiel oben. Ich bin nicht sicher, wie ich überprüfen soll, ob der Datenspeicher tatsächlich aufgerufen wurde, ohne eine Zusicherung für "MustHaveBeenCalled" zu verwenden. Was Punkt 2 betrifft, haben Sie absolut Recht. Das habe ich nach all den Änderungen erledigt, aber ich wollte nur sicherstellen, dass mein Ansatz im Allgemeinen den anerkannten TDD-Praktiken entspricht. Vielen Dank!
Landon
@Landon Es gibt Fälle, in denen Interaktionstests besser geeignet sind. Überprüfen Sie beispielsweise, ob eine Datenbank oder ein Webdienst aufgerufen wurde. Grundsätzlich immer dann, wenn Sie Ihren Test von einem externen Dienst trennen müssen.
Jhewlett
@Landon Ich bin ein "überzeugter Klassiker", daher bin ich nicht sehr erfahren mit interationsbasierten Tests ... Aber Sie müssen keine Aussage für "MustHaveBeenCalled" machen. Wenn Sie eine Einfügung testen, können Sie anhand einer Abfrage feststellen, ob sie eingefügt wurde. PS: Ich verwende Stubs aus Leistungsgründen, wenn ich alles außer der Datenbankebene teste.
Hbas
@jhewlett Zu diesem Schluss bin ich auch gekommen. Vielen Dank!
Landon
@Hbas Es ist keine Datenbank zum Abfragen vorhanden. Ich stimme zu, dass dies der einfachste Weg wäre, wenn ich einen hätte, aber ich füge diesen einem OneNote-Notizbuch hinzu. Das Beste, was ich tun kann, ist, meiner Interop-Helferklasse eine Get-Methode hinzuzufügen, um zu versuchen, die Seite zu ziehen. Ich KÖNNTE den Test schreiben, um das zu tun, aber ich hatte das Gefühl, zwei Dinge gleichzeitig zu testen: Habe ich das gespeichert? und Ruft meine Helferklasse die Seiten korrekt ab? Obwohl ich denke, dass Ihre Tests irgendwann auf anderem Code beruhen müssen, der an anderer Stelle getestet wurde. Vielen Dank!
Landon
10

... Ich kann nicht anders, als das Gefühl zu haben, ich hätte das schneller bemerkt, wenn ich das Design im Voraus entworfen hätte, anstatt es während des TDD-Prozesses entwerfen zu lassen ...

Vielleicht, vielleicht nicht

Einerseits hat TDD einwandfrei funktioniert und Ihnen automatisierte Tests während des Aufbaus der Funktionalität ermöglicht, die sich sofort lösten, wenn Sie die Benutzeroberfläche ändern mussten.

Wenn Sie dagegen mit der Funktion auf hoher Ebene (SaveProject) anstelle einer Funktion auf niedrigerer Ebene (CreateProject) begonnen hätten, wären Sie möglicherweise früher auf fehlende Parameter gestoßen.

Andererseits hättest du es vielleicht nicht. Es ist ein unwiederholbares Experiment.

Aber wenn Sie eine Lektion für das nächste Mal suchen: Beginnen Sie oben. Und denken Sie an das Design, so oft Sie möchten.

Steven A. Lowe
quelle
0

https://frontendmasters.com/courses/angularjs-and-code-testability/ Von ungefähr 2:22:00 bis zum Ende (ungefähr 1 Stunde). Es tut mir leid, dass das Video nicht kostenlos ist, aber ich habe kein kostenloses gefunden, das es so gut erklärt.

Eine der besten Präsentationen zum Schreiben von testbarem Code befindet sich in dieser Lektion. Es ist eine AngularJS-Klasse, aber der Testteil handelt von Java-Code, vor allem, weil das, wovon er spricht, nichts mit der Sprache zu tun hat, und alles, was mit dem Schreiben von gut testbarem Code zu tun hat.

Die Magie besteht darin, testbaren Code zu schreiben, anstatt Code-Tests zu schreiben. Es geht nicht darum, Code zu schreiben, der vorgibt, ein Benutzer zu sein.

Er verbringt auch einige Zeit damit, die Spezifikation in Form von Testbehauptungen zu schreiben.

Bootskodierer
quelle