So korrigieren Sie einen Fehler im Test nach dem Schreiben der Implementierung

21

Was ist die beste Vorgehensweise in TDD, wenn der Test nach korrekter Implementierung immer noch fehlschlägt (weil ein Fehler im Test vorliegt)?

Angenommen, Sie möchten die folgende Funktion entwickeln:

int add(int a, int b) {
    return a + b;
}

Angenommen, wir entwickeln es in den folgenden Schritten:

  1. Schreibtest (noch keine Funktion):

    // test1
    Assert.assertEquals(5, add(2, 3));
    

    Ergibt einen Kompilierungsfehler.

  2. Schreiben Sie eine Dummy-Funktionsimplementierung:

    int add(int a, int b) {
        return 5;
    }
    

    Ergebnis: bestanden test1.

  3. Fügen Sie einen weiteren Testfall hinzu:

    // test2 -- notice the wrong expected value (should be 11)!
    Assert.assertEquals(12, add(5, 6));
    

    Ergebnis: test2nicht bestanden, test1noch bestanden.

  4. Schreiben Sie eine echte Implementierung:

    int add(int a, int b) {
        return a + b;
    }
    

    Ergebnis: test1noch bestanden, test2noch nicht bestanden (seit 11 != 12).

In diesem speziellen Fall: Wäre es besser:

  1. richtig test2, und sehen, dass es jetzt passiert, oder
  2. Löschen Sie den neuen Teil der Implementierung (dh gehen Sie zurück zu Schritt 2 oben), korrigieren test2Sie ihn und lassen Sie ihn fehlschlagen. Führen Sie dann die korrekte Implementierung wieder ein (Schritt 4 oben).

Oder gibt es einen anderen, klügeren Weg?

Obwohl ich verstehe, dass das Beispielproblem eher trivial ist, interessiert mich, was in dem generischen Fall zu tun ist, der komplexer sein könnte als die Addition von zwei Zahlen.

BEARBEITEN (als Antwort auf die Antwort von @Thomas Junk):

Der Schwerpunkt dieser Frage liegt auf dem, was TDD in einem solchen Fall vorschlägt, und nicht auf der "Universal Best Practice", um guten Code oder Tests zu erzielen (die sich möglicherweise von der TDD-Methode unterscheiden).

Attilio
quelle
3
Refactoring gegen den roten Balken ist ein relevantes Konzept.
RubberDuck
5
Natürlich müssen Sie TDD auf Ihrem TDD ausführen.
Blrfl
17
Wenn mich jemals jemand fragt, warum ich TDD skeptisch gegenüber stehe, werde ich ihn auf diese Frage hinweisen. Das ist Kafkaesque.
Traubenfuchs
@Blrfl das ist, was Xibit uns sagt »Ich habe das TDD in TDD eingefügt, damit Sie TDD während des TDDing durchführen können«: D
Thomas Junk
3
@Traubenfuchs Ich gebe zu, die Frage erscheint auf den ersten Blick albern und ich bin kein Befürworter von "immer TDD machen", aber ich glaube, es ist von großem Vorteil, wenn ein Test fehlschlägt und dann Code geschrieben wird, der den Test besteht (Worum geht es denn eigentlich in dieser Frage?).
Vincent Savard

Antworten:

31

Das absolut Entscheidende ist, dass der Test sowohl erfolgreich als auch nicht erfolgreich ist.

Ob Sie den Code löschen, damit der Test fehlschlägt, und ihn dann neu schreiben oder in die Zwischenablage verschieben, um ihn später wieder einzufügen, spielt keine Rolle. TDD hat nie gesagt, dass Sie etwas neu eingeben müssen. Es möchte wissen, dass der Test nur dann bestanden wird, wenn er bestanden werden soll, und nur dann, wenn er nicht bestanden werden soll.

Wenn Sie sehen, ob der Test bestanden wurde oder nicht, testen Sie den Test. Vertrauen Sie niemals einem Test, den Sie noch nie gesehen haben.


Refactoring gegen die rote Leiste enthält formale Schritte zum Refactoring eines Funktionstests:

  • Führen Sie den Test aus
    • Beachten Sie den grünen Balken
    • Brechen Sie den Code, der getestet wird
  • Führen Sie den Test aus
    • Beachten Sie den roten Balken
    • Refactor den Test
  • Führen Sie den Test aus
    • Beachten Sie den roten Balken
    • Öffnen Sie den zu testenden Code
  • Führen Sie den Test aus
    • Beachten Sie den grünen Balken

Wir überarbeiten jedoch keinen funktionierenden Test. Wir müssen einen Buggy-Test umbauen. Ein Problem ist Code, der eingeführt wurde, während nur dieser Test es abdeckte. Ein solcher Code sollte zurückgesetzt und wieder eingeführt werden, sobald der Test behoben ist.

Wenn dies nicht der Fall ist und die Codeabdeckung aufgrund anderer den Code abdeckender Tests kein Problem darstellt, können Sie den Test transformieren und als grünen Test einführen.

Hier wird auch Code zurückgesetzt, aber gerade genug, um den Test fehlschlagen zu lassen. Wenn das nicht ausreicht, um den gesamten eingeführten Code abzudecken, während nur der Buggy-Test abdeckt, brauchen wir ein größeres Code-Rollback und mehr Tests.

Führen Sie einen Grüntest ein

  • Führen Sie den Test aus
    • Beachten Sie den grünen Balken
    • Brechen Sie den Code, der getestet wird
  • Führen Sie den Test aus
    • Beachten Sie den roten Balken
    • Öffnen Sie den zu testenden Code
  • Führen Sie den Test aus
    • Beachten Sie den grünen Balken

Das Aufbrechen des Codes kann darin bestehen, Code auskommentieren oder an eine andere Stelle zu verschieben, um ihn später wieder einzufügen. Dies zeigt uns den Umfang des Codes, den der Test abdeckt.

Bei den letzten beiden Läufen kehren Sie direkt in den normalen Rot-Grün-Zyklus zurück. Sie müssen nur etwas einfügen, anstatt etwas zu tippen, um den Code freizugeben und den Test zu bestehen. Stellen Sie also sicher, dass Sie nur so viel einfügen, dass der Test bestanden wird.

Das Gesamtmuster ist hier, um zu sehen, wie die Farbe des Tests die Art und Weise ändert, wie wir es erwarten. Beachten Sie, dass dies zu einer Situation führt, in der Sie kurzzeitig einen nicht vertrauenswürdigen Grüntest durchführen. Achten Sie darauf, dass Sie nicht unterbrochen werden und vergessen, wo Sie sich gerade befinden.

Mein Dank geht an RubberDuck für den Link Embracing the Red Bar .

kandierte_orange
quelle
2
Diese Antwort gefällt mir am besten: Es ist wichtig, dass der Test mit falschem Code fehlschlägt, sodass ich den Code lösche / auskommentiere, die Tests korrigiere und sehe, dass sie fehlschlagen den Test) und korrigieren Sie den Code, damit er funktioniert. Es ist sehr XP, es komplett zu löschen und neu zu schreiben, aber manchmal muss man einfach pragmatisch sein. ;)
GolezTrol
@GolezTrol Ich denke, meine Antwort sagt das Gleiche. Ich würde mich über jedes Feedback freuen, das Sie dazu erhalten haben, ob dies unklar ist.
Jonrsharpe
@jonrsharpe Deine Antwort ist auch gut, und ich habe sie hochgestuft, bevor ich sie überhaupt gelesen habe. Aber wenn Sie beim Zurücksetzen des Codes sehr streng sind, schlägt CandiedOrange einen pragmatischeren Ansatz vor, der mich mehr anspricht.
GolezTrol
@GolezTrol Ich habe nicht gesagt, wie der Code zurückgesetzt werden soll. Kommentieren Sie es aus, schneiden Sie es aus und fügen Sie es ein, verstauen Sie es und verwenden Sie den Verlauf Ihrer IDE. es ist nicht wirklich wichtig. Das Entscheidende ist, warum Sie es tun: So können Sie überprüfen, ob Sie den richtigen Fehler erhalten. Ich habe bearbeitet, hoffentlich zu klären.
Jonrsharpe
10

Was ist das Gesamtziel , wollen Sie erreichen?

  • Schöne Tests machen?

  • Herstellung der korrekten Umsetzung?

  • TTD religiös richtig machen ?

  • Nichts des oben Genannten?

Vielleicht überdenken Sie Ihre Beziehung zu Tests und zum Testen.

Tests geben keine Gewähr für die Richtigkeit einer Implementierung. Wenn alle Tests bestanden sind, sagt dies nichts darüber aus, ob Ihre Software das tut, was sie sollte. Es gibt keine wesentlichen Aussagen über Ihre Software.

Nehmen Sie Ihr Beispiel:

Die "korrekte" Implementierung des Zusatzes wäre der Code, der äquivalent zu ist a+b. Und solange Ihr Code das tut , würden Sie sagen, dass der Algorithmus in seiner Funktion korrekt ist und korrekt implementiert ist.

int add(int a, int b) {
    return a + b;
}

Auf dem ersten Blick , wir beide würden zustimmen, dass dies ist die Umsetzung einer Ergänzung.

Aber was wir tun , ist wirklich nicht zu sagen, dass dieser Code ist die Umsetzung die additiones nur verhält sich bis zu einem gewissen Grad , wie man: Man denke an Integer - Überlauf .

Ganzzahliger Überlauf tritt im Code auf, aber nicht im Konzept von addition. Also: Ihr Code verhält sich bis zu einem gewissen Grad wie das Konzept von addition, ist es aber nicht addition.

Dieser eher philosophische Standpunkt hat mehrere Konsequenzen.

Und man kann sagen, dass Tests nichts anderes sind als Annahmen über das erwartete Verhalten Ihres Codes. Beim Testen Sie den Code, können Sie (vielleicht) nie sicher machen, Ihre Implementierung ist richtig , das Beste , was Sie sagen kann , ist, dass Ihre Erwartungen an , welche Ergebnisse der Code waren liefert oder nicht erfüllt; Sei es, dass dein Code falsch ist, sei es, dass dein Test falsch ist oder sei es, dass beide falsch sind.

Mithilfe nützlicher Tests können Sie Ihre Erwartungen an die Funktionsweise des Codes festlegen : Solange ich meine Erwartungen nicht ändere und der geänderte Code das erwartete Ergebnis liefert, kann ich mir sicher sein, dass die von mir getroffenen Annahmen zutreffen Die Ergebnisse scheinen zu klappen.

Das hilft nicht, wenn Sie die falschen Annahmen getroffen haben. aber hey! Zumindest beugt es Schizophrenie vor: Erwarten Sie unterschiedliche Ergebnisse, wenn es keine geben sollte.


tl; dr

Was ist die beste Vorgehensweise in TDD, wenn der Test nach korrekter Implementierung immer noch fehlschlägt (weil ein Fehler im Test vorliegt)?

Ihre Tests sind Annahmen über das Verhalten des Codes. Wenn Sie guten Grund zur Annahme haben, dass Ihre Implementierung richtig ist, korrigieren Sie den Test und prüfen Sie, ob diese Annahme zutrifft.

Thomas Junk
quelle
1
Ich denke, die Frage nach den allgemeinen Zielen ist ziemlich wichtig, danke, dass Sie sie angesprochen haben. Für mich ist der höchste Preis der folgende: 1. korrekte Implementierung 2. "nette" Tests (oder, ich würde eher sagen, "nützliche" / "gut gestaltete" Tests). Ich sehe TDD als ein mögliches Instrument, um diese beiden Ziele zu erreichen. Obwohl ich TDD nicht unbedingt religiös verfolgen möchte, interessiert mich im Zusammenhang mit dieser Frage vor allem die TDD-Perspektive. Ich werde die Frage bearbeiten, um dies zu klären.
Attilio
Würden Sie also einen Test schreiben, der auf Überlauf prüft und bestanden wird, wenn er auftritt, oder würden Sie ihn zum Scheitern bringen, wenn er auftritt, weil der Algorithmus addiert und Überlauf die falsche Antwort liefert?
Jerry Jeremiah
1
Mein Punkt ist: Was Ihre Tests abdecken sollten, hängt von Ihrem Anwendungsfall ab. Für einen Anwendungsfall, bei dem Sie eine Reihe von einzelnen Ziffern addieren, ist der Algorithmus gut genug . Wenn Sie wissen, dass es sehr wahrscheinlich ist, dass Sie "große Zahlen" addieren, datatypeist das eindeutig die falsche Wahl. Ein Test würde ergeben: Ihre Erwartung wäre »funktioniert für große Zahlen« und wird in einigen Fällen nicht erfüllt. Dann wäre die Frage, wie mit diesen Fällen umgegangen werden soll. Sind sie Eckfälle? Wann ja, wie soll man damit umgehen? Vielleicht helfen einige Quard-Klauseln, ein größeres Durcheinander zu verhindern. Die Antwort ist kontextgebunden.
Thomas Junk
7

Sie müssen wissen, dass der Test fehlschlagen wird, wenn die Implementierung falsch ist. Dies ist nicht dasselbe wie das Bestehen, wenn die Implementierung richtig ist. Daher sollten Sie den Code vor dem Korrigieren des Tests wieder in einen Zustand versetzen, in dem Sie einen Fehler erwarten , und sicherstellen, dass der Fehler aus dem von Ihnen erwarteten Grund (z. B. 5 != 12) und nicht aus einem anderen, von Ihnen nicht vorhergesagten Grund auftritt.

jonrsharpe
quelle
Wie können wir überprüfen, ob der Test aus dem erwarteten Grund fehlschlägt?
Basilevs
2
@Basilevs Sie: 1. Stellen Sie eine Hypothese auf, was der Grund für das Scheitern sein sollte; 2. Führen Sie den Test durch. und 3. die resultierende Fehlermeldung lesen und vergleichen. Gelegentlich schlägt dies auch Möglichkeiten vor, wie Sie den Test umschreiben können, um einen aussagekräftigeren Fehler zu erhalten (z. B. assertTrue(5 == add(2, 3))eine weniger nützliche Ausgabe, als assertEqual(5, add(2, 3))wenn beide dasselbe testen).
Jonrsharpe
Es ist noch unklar, wie dieses Prinzip hier angewendet werden soll. Ich habe eine Hypothese - Test gibt einen konstanten Wert zurück. Wie würde ein erneuter Test sicherstellen, dass ich Recht habe? Um das zu testen, brauche ich natürlich noch einen Test. Ich schlage vor, der Antwort ein explizites Beispiel hinzuzufügen.
Basilevs
1
@Basilevs was? Ihre Hypothese hier in Schritt 3 wäre "Der Test schlägt fehl, weil 5 ungleich 12 ist" . Durch Ausführen des Tests wird angezeigt, ob der Test aus diesem Grund fehlschlägt. In diesem Fall fahren Sie fort, oder aus einem anderen Grund. In diesem Fall finden Sie heraus, warum. Vielleicht ist dies ein Sprachproblem, aber mir ist nicht klar, was Sie vorschlagen.
Jonrsharpe
5

In diesem speziellen Fall, wenn Sie die 12 in eine 11 ändern und der Test nun erfolgreich ist, haben Sie den Test und die Implementierung meiner Meinung nach gut getestet. Es ist also nicht viel erforderlich, zusätzliche Rahmen zu durchlaufen.

Das gleiche Problem kann jedoch in komplexeren Situationen auftreten, z. B. wenn Sie einen Fehler in Ihrem Setup-Code haben. In diesem Fall sollten Sie nach dem Korrigieren des Tests wahrscheinlich versuchen, die Implementierung so zu ändern, dass der betreffende Test fehlschlägt, und dann die Änderung rückgängig machen. Wenn das Zurücksetzen der Implementierung der einfachste Weg ist, dann ist das in Ordnung. In Ihrem Beispiel könnten Sie a + bzu a + aoder mutieren a * b.

Wenn Sie alternativ die Behauptung leicht ändern und feststellen, dass der Test fehlschlägt, kann dies beim Testen des Tests sehr effektiv sein.

Vaughn Cato
quelle
0

Ich würde sagen, dies ist ein Fall für Ihr Lieblingsversionskontrollsystem:

  1. Führen Sie die Korrektur des Tests durch und behalten Sie Ihre Codeänderungen in Ihrem Arbeitsverzeichnis bei.
    Bestätigen Sie mit einer entsprechenden Nachricht Fixed test ... to expect correct output.

    Mit gitkönnte diese Anwendung benötigen , git add -pwenn der Test und Implementierung ist in der gleichen Datei, sonst kann man natürlich die Bühne einfach die beiden Dateien getrennt.

  2. Übernehmen Sie den Implementierungscode.

  3. Gehen Sie in der Zeit zurück, um die in Schritt 1 vorgenommene Festschreibung zu testen, und stellen Sie sicher, dass der Test tatsächlich fehlschlägt .

Sie sehen, auf diese Weise verlassen Sie sich nicht auf Ihre Bearbeitungsfähigkeiten, um Ihren Implementierungscode aus dem Weg zu räumen, während Sie Ihren fehlgeschlagenen Test testen. Sie setzen Ihr VCS ein, um Ihre Arbeit zu speichern und sicherzustellen, dass der korrekt aufgezeichnete VCS-Verlauf sowohl den fehlerhaften als auch den bestandenen Test enthält.

cmaster
quelle