Was ist zu tun, wenn TDD-Tests neue Funktionen ergeben, die ebenfalls getestet werden müssen?

13

Was tun Sie, wenn Sie einen Test schreiben und an den Punkt gelangen, an dem Sie den Test bestehen müssen, und wenn Sie feststellen, dass Sie eine zusätzliche Funktionalität benötigen, die in ihre eigene Funktion unterteilt werden sollte? Diese neue Funktion muss ebenfalls getestet werden, aber der TDD-Zyklus sagt, dass ein Test fehlschlagen soll. Lassen Sie ihn bestehen und überarbeiten Sie ihn. Wenn ich mich in der Phase befinde, in der ich versuche, meinen Test zu bestehen, darf ich keinen weiteren fehlgeschlagenen Test starten, um die neuen Funktionen zu testen, die ich implementieren muss.

Zum Beispiel schreibe ich eine Punktklasse , die eine Funktion WillCollideWith ( LineSegment ) hat :

public class Point {
    // Point data and constructor ...

    public bool CollidesWithLine(LineSegment lineSegment) {
        Vector PointEndOfMovement = new Vector(Position.X + Velocity.X,
                                               Position.Y + Velocity.Y);
        LineSegment pointPath = new LineSegment(Position, PointEndOfMovement);
        if (lineSegment.Intersects(pointPath)) return true;
        return false;
    }
}

Ich habe gerade einen Test für CollidesWithLine geschrieben, als mir klar wurde, dass ich eine LineSegment.Intersects ( LineSegment ) -Funktion benötigen würde . Aber sollte ich einfach aufhören, was ich in meinem Testzyklus mache, um diese neue Funktionalität zu erstellen? Das scheint das "Rot, Grün, Refaktor" -Prinzip zu brechen.

Sollte ich nur den Code schreiben, der diese lineSegments-Überschneidung in der CollidesWithLine- Funktion erkennt, und ihn nach der Ausführung umgestalten? Das würde in diesem Fall funktionieren , da ich über LineSegment auf die Daten zugreifen kann. Was ist jedoch mit den Fällen, in denen diese Art von Daten privat ist?

Joshua Harris
quelle

Antworten:

14

Kommentieren Sie einfach Ihren Test und den aktuellen Code aus (oder speichern Sie ihn), damit Sie die Uhr auf den Beginn des Zyklus zurückdrehen können. Beginnen Sie dann mit dem LineSegment.Intersects(LineSegment)Test / Code / Refactor. Wenn das erledigt ist, kommentieren Sie Ihren vorherigen Test / Code (oder ziehen Sie ihn aus dem Vorrat) und halten Sie sich an den Zyklus.

Javier
quelle
Wie ist das anders, wenn man es einfach ignoriert und später darauf zurückkommt?
Joshua Harris
1
nur kleine Details: Es gibt keinen zusätzlichen "Ignorier mich" -Test in Berichten, und wenn Sie Stashes verwenden, ist der Code nicht vom "sauberen" Fall zu unterscheiden.
Javier
Was ist ein Versteck? ist das wie Versionskontrolle?
Joshua Harris
1
Einige VCS implementieren es als Feature (zumindest Git und Fossil). Sie können eine Änderung entfernen, aber für eine spätere erneute Anwendung speichern. Manuell ist es nicht schwer, ein Diff zu speichern und zum letzten Zustand zurückzukehren. Später wendest du das Diff erneut an und machst weiter.
Javier
6

Im TDD-Zyklus:

In der Phase "Den Test bestehen" sollten Sie die einfachste Implementierung schreiben , die den Test bestehen wird . Um Ihren Test zu bestehen, haben Sie sich entschieden, einen neuen Mitarbeiter zu erstellen, der die fehlende Logik handhabt, da es möglicherweise zu viel Arbeit war, Ihre Punkteklasse zu füllen, um Ihren Test zu bestehen. Hier liegt das Problem. Ich nehme an, der Test, den Sie machen wollten, war ein zu großer Schritt . Ich denke, das Problem liegt in Ihrem Test selbst. Sie sollten diesen Test löschen / kommentieren und einen einfacheren Test finden, mit dem Sie einen kleinen Schritt machen können, ohne den Teil LineSegment.Intersects (LineSegment) einzuführen. Wenn Sie diesen Test bestanden haben, können Sie ihn umgestaltenIhren Code (hier wenden Sie das SRP-Prinzip an), indem Sie diese neue Logik in eine LineSegment.Intersects-Methode (LineSegment) verschieben. Ihre Tests werden weiterhin bestanden, da Sie kein Verhalten geändert haben, sondern nur Code verschoben haben.

Auf Ihrer aktuellen Designlösung

Für mich ist jedoch das Problem, dass Sie gegen das Prinzip der Einzelverantwortung verstoßen, noch schwerwiegender . Die Rolle eines Punktes ist ... ein Punkt zu sein, das ist alles. Es ist nicht klug, ein Punkt zu sein, es ist gerecht und x- und y-Wert. Punkte sind Werttypen . Dies gilt auch für Segmente. Segmente sind Wertetypen, die aus zwei Punkten bestehen. Sie können beispielsweise ein bisschen "Schlauheit" enthalten, um ihre Länge basierend auf ihrer Punkteposition zu berechnen. Aber das ist es.

Die Entscheidung, ob ein Punkt und ein Segment kollidieren, liegt in eigener Verantwortung. Und es ist mit Sicherheit zu viel Arbeit, als dass ein Punkt oder ein Segment alleine fertig werden könnte. Es kann nicht zur Point-Klasse gehören, da Points sonst Informationen zu Segmenten erhält. Und es kann nicht zu Segmenten gehören, da Segmente bereits die Verantwortung haben, sich um die Punkte innerhalb des Segments zu kümmern und möglicherweise auch die Länge des Segments selbst zu berechnen.

Diese Verantwortung sollte also einer anderen Klasse gehören, wie zum Beispiel einem "PointSegmentCollisionDetector", der eine Methode wie die folgende haben würde:

bool AreInCollision (Punkt p, Segment s)

Und das ist etwas, das Sie separat von Punkten und Segmenten testen würden.

Das Schöne an diesem Design ist, dass Sie Ihren Kollisionsdetektor jetzt anders implementieren können. So wäre es zum Beispiel einfach, Ihre Game-Engine (ich nehme an, Sie schreiben ein Spiel: p) einem Benchmark zu unterziehen, indem Sie Ihre Kollisionserkennungsmethode zur Laufzeit wechseln. Oder um zur Laufzeit visuelle Überprüfungen / Experimente zwischen verschiedenen Kollisionserkennungsstrategien durchzuführen.

Wenn Sie diese Logik in Ihre Punkteklasse aufnehmen, sperren Sie momentan die Dinge ein und verlagern zu viel Verantwortung auf die Punkteklasse.

Hoffe es macht Sinn,

foobarcode
quelle
Sie haben Recht, dass ich versucht habe, eine zu große Änderung zu testen, und ich denke, Sie haben Recht, dies in eine Kollisionsklasse aufzuteilen, aber das bringt mich dazu, eine völlig neue Frage zu stellen, bei der Sie mir möglicherweise helfen können: Sollte ich Verwenden Sie eine Schnittstelle, wenn die Methoden nur ähnlich sind? .
Joshua Harris
2

Am einfachsten ist es, eine Schnittstelle für LineSegment zu extrahieren und die Methodenparameter so zu ändern, dass sie die Schnittstelle übernehmen. Dann könnten Sie das Eingabezeilensegment verspotten und die Intersect-Methode unabhängig codieren / testen.

Dan Lyons
quelle
1
Ich weiß, dass dies die TDD-Methode ist, die ich am häufigsten höre, aber ein ILineSegment macht keinen Sinn. Es ist eine Sache, eine externe Ressource oder etwas, das in vielen Formen vorliegen könnte, herauszulesen, aber ich kann keinen Grund erkennen, warum ich jemals eine der Funktionen an etwas anderes als ein Liniensegment anhängen würde.
Joshua Harris
0

Mit jUnit4 können Sie die @IgnoreAnnotation für die Tests verwenden, die Sie verschieben möchten.

Fügen Sie die Anmerkung zu jeder Methode hinzu, die Sie verschieben möchten, und setzen Sie das Schreiben von Tests für die erforderliche Funktionalität fort. Kreise zurück, um die älteren Testfälle später zu überarbeiten.

Bakoyaro
quelle