Angenommen, Sie schreiben einen TDD-Stil für ein Yahtzee-Spiel. Sie möchten den Teil des Codes testen, der bestimmt, ob ein Satz mit fünf Würfeln ein volles Haus ist oder nicht. Soweit ich weiß, befolgen Sie bei TDD folgende Grundsätze:
- Schreiben Sie zuerst die Tests
- Schreiben Sie das Einfachste, was funktioniert
- Verfeinern und umgestalten
Ein erster Test könnte also so aussehen:
public void Returns_true_when_roll_is_full_house()
{
FullHouseTester sut = new FullHouseTester();
var actual = sut.IsFullHouse(1, 1, 1, 2, 2);
Assert.IsTrue(actual);
}
Wenn Sie dem Befehl "Schreiben Sie so einfach IsFullHouse
wie möglich" folgen, sollten Sie die Methode folgendermaßen schreiben :
public bool IsFullHouse(int roll1, int roll2, int roll3, int roll4, int roll5)
{
if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
{
return true;
}
return false;
}
Dies führt zu einem grünen Test, aber die Implementierung ist unvollständig.
Sollten Sie jede mögliche gültige Kombination (sowohl von Werten als auch von Positionen) für ein volles Haus testen? Das scheint die einzige Möglichkeit zu sein, absolut sicher zu sein, dass Ihr IsFullHouse
Code vollständig getestet und korrekt ist, aber es klingt auch ziemlich verrückt, dies zu tun.
Wie würden Sie so etwas testen?
Aktualisieren
Erik und Kilian weisen darauf hin, dass die Verwendung von Literalen in der ersten Implementierung für einen Grüntest möglicherweise nicht die beste Idee ist. Ich möchte erklären, warum ich das getan habe und diese Erklärung passt nicht in einen Kommentar.
Meine praktischen Erfahrungen mit Unit-Tests (insbesondere mit einem TDD-Ansatz) sind sehr begrenzt. Ich erinnere mich, eine Aufnahme von Roy Osheroves TDD Masterclass auf Tekpub gesehen zu haben. In einer der Folgen baut er einen String Calculator TDD-Stil auf. Die vollständige Spezifikation des String-Rechners finden Sie hier: http://osherove.com/tdd-kata-1/
Er beginnt mit einem Test wie diesem:
public void Add_with_empty_string_should_return_zero()
{
StringCalculator sut = new StringCalculator();
int result = sut.Add("");
Assert.AreEqual(0, result);
}
Dies führt zu dieser ersten Implementierung der Add
Methode:
public int Add(string input)
{
return 0;
}
Dann wird dieser Test hinzugefügt:
public void Add_with_one_number_string_should_return_number()
{
StringCalculator sut = new StringCalculator();
int result = sut.Add("1");
Assert.AreEqual(1, result);
}
Und die Add
Methode wurde überarbeitet:
public int Add(string input)
{
if (input.Length == 0)
{
return 0;
}
return 1;
}
Nach jedem Schritt sagt Roy "Schreibe das Einfachste, was funktionieren wird".
Daher dachte ich, ich würde diesen Ansatz ausprobieren, wenn ich ein Yahtzee-Spiel im TDD-Stil spielen wollte.
quelle
if (roll1 == 1 && roll2 == 1 && roll3 == 1 && roll4 == 2 && roll5 == 2)
Antworten:
Es gibt bereits viele gute Antworten auf diese Frage, und ich habe einige von ihnen kommentiert und bewertet. Dennoch möchte ich einige Gedanken hinzufügen.
Flexibilität ist nichts für Anfänger
Das OP gibt eindeutig an, dass er keine Erfahrung mit TDD hat, und ich denke, eine gute Antwort muss dies berücksichtigen. In der Terminologie des Dreyfus-Modells für den Erwerb von Fähigkeiten ist er wahrscheinlich ein Neuling . Es ist nichts Falsches daran, Anfänger zu sein - wir sind alle Anfänger, wenn wir anfangen, etwas Neues zu lernen. Das Dreyfus-Modell erklärt jedoch, dass Anfänger durch gekennzeichnet sind
Das ist keine Beschreibung eines Persönlichkeitsmangels, daher gibt es keinen Grund, sich dafür zu schämen - es ist eine Phase, die wir alle durchlaufen müssen, um etwas Neues zu lernen.
Dies gilt auch für TDD.
Obwohl ich vielen anderen Antworten hier zustimme, dass TDD nicht dogmatisch sein muss und dass es manchmal vorteilhafter sein kann, auf alternative Weise zu arbeiten, hilft das niemandem, wenn er gerade erst anfängt. Wie können Sie diskretionär urteilen, wenn Sie keine Erfahrung haben?
Wenn ein Anfänger den Rat annimmt, dass es manchmal in Ordnung ist, kein TDD zu machen, wie kann er feststellen, wann es in Ordnung ist, das TDD zu überspringen?
Ohne Erfahrung oder Anleitung kann ein Anfänger TDD nur jedes Mal verlassen, wenn es zu schwierig wird. Das ist die menschliche Natur, aber kein guter Weg, um zu lernen.
Hören Sie sich die Tests an
Wenn es immer schwieriger wird, TDD zu verlassen, müssen Sie sich einen der wichtigsten Vorteile von TDD entgehen lassen. Tests bieten frühzeitiges Feedback zur API des SUT. Wenn der Test schwer zu schreiben ist, ist dies ein wichtiges Zeichen dafür, dass das SUT schwer zu verwenden ist.
Aus diesem Grund lautet eine der wichtigsten Botschaften von GOOS : Hören Sie auf Ihre Tests!
Bei dieser Frage war meine erste Reaktion auf die vorgeschlagene API des Yahtzee-Spiels und die Diskussion über die Kombinatorik auf dieser Seite, dass dies ein wichtiges Feedback zur API ist.
Muss die API Würfelwürfe als geordnete Folge von ganzen Zahlen darstellen? Für mich dieser Geruch von primitiver Besessenheit . Deshalb freute ich mich über die Antwort von tallseth, die die Einführung einer
Roll
Klasse vorschlug . Ich denke, das ist ein ausgezeichneter Vorschlag.Ich denke jedoch, dass einige der Kommentare zu dieser Antwort falsch sind. Was TDD dann vorschlägt, ist, dass, sobald Sie die Idee haben, dass eine
Roll
Klasse eine gute Idee wäre, Sie die Arbeit am ursprünglichen SUT unterbrechen und beginnen, dieRoll
Klasse mit TDD zu bearbeiten .Ich stimme zu, dass TDD mehr auf den „glücklichen Weg“ als auf umfassende Tests abzielt, aber es hilft trotzdem, das System in verwaltbare Einheiten zu zerlegen. Eine
Roll
Klasse klingt wie etwas, das man viel einfacher zu Ende bringen könnte.Sobald die
Roll
Klasse ausreichend entwickelt ist, würden Sie zum ursprünglichen SUT zurückkehren und es in Bezug aufRoll
Eingaben ausarbeiten .Der Vorschlag eines Test-Helfers impliziert nicht notwendigerweise Zufälligkeit - es ist nur eine Möglichkeit, den Test lesbarer zu machen.
Eine andere Möglichkeit, Eingaben in Bezug auf
Roll
Instanzen zu analysieren und zu modellieren, wäre die Einführung eines Test Data Builder .Rot / Grün / Refaktor ist ein dreistufiger Prozess
Obwohl ich der allgemeinen Meinung zustimme, dass (wenn Sie über ausreichende Erfahrung mit TDD verfügen) Sie sich nicht strikt an TDD halten müssen, denke ich, dass dies bei einer Yahtzee-Übung ein ziemlich schlechter Rat ist. Obwohl ich die Details der Yahtzee-Regeln nicht kenne, sehe ich hier kein überzeugendes Argument dafür, dass Sie sich nicht rigoros an den Red / Green / Refactor-Prozess halten und dennoch ein angemessenes Ergebnis erzielen können.
Was die meisten hier zu vergessen scheinen, ist die dritte Stufe des Rot / Grün / Refaktor-Prozesses. Zuerst schreibst du den Test. Dann schreiben Sie die einfachste Implementierung, die alle Tests besteht. Dann überarbeiten Sie.
Hier, in diesem dritten Zustand, können Sie all Ihre beruflichen Fähigkeiten unter Beweis stellen. Hier dürfen Sie über den Code nachdenken.
Ich denke jedoch, es ist eine Ausrede, zu behaupten, dass Sie nur "das einfachste schreiben sollten, was möglich ist, und das nicht völlig geisteskrank und offensichtlich falsch , was funktioniert". Wenn Sie (glaube ich) im Voraus genug über die Implementierung wissen, wird alles , was nicht zur vollständigen Lösung gehört, offensichtlich falsch sein . Was den Rat betrifft, ist dies für einen Anfänger ziemlich nutzlos.
Was wirklich passieren sollte, ist, dass, wenn Sie alle Tests mit einer offensichtlich falschen Implementierung bestehen können, dies die Rückmeldung ist, dass Sie einen weiteren Test schreiben sollten .
Es ist überraschend, wie oft Sie dies tun, um eine völlig andere Implementierung zu erreichen als die, an die Sie zuerst gedacht hatten. Manchmal ist die Alternative, die so wächst, besser als Ihr ursprünglicher Plan.
Strenge ist ein Lernwerkzeug
Es ist sehr sinnvoll, sich an strenge Prozesse wie Rot / Grün / Refaktor zu halten, solange man lernt. Es zwingt den Lernenden, Erfahrung mit TDD zu sammeln, nicht nur, wenn es einfach ist, sondern auch, wenn es schwierig ist.
Nur wenn Sie alle harten Teile gemeistert haben, sind Sie in der Lage, eine fundierte Entscheidung darüber zu treffen, wann Sie vom „wahren“ Pfad abweichen sollten. Dann beginnt man seinen eigenen Weg zu finden.
quelle
Als Haftungsausschluss ist dies TDD, wie ich es praktiziere, und wie Kilian treffend hervorhebt, wäre ich vorsichtig mit jedem, der vorschlug, dass es einen richtigen Weg gibt, es zu praktizieren. Aber vielleicht hilft es dir ...
Das Einfachste, was Sie tun können, um Ihren Test zu bestehen, ist Folgendes:
Dies ist wichtig, da dies nicht auf eine TDD-Praxis zurückzuführen ist, sondern weil Hardcording in all diesen Literalen keine wirklich gute Idee ist. Eines der schwierigsten Dinge, die Sie mit TDD tun können, ist, dass es keine umfassende Teststrategie ist - es ist eine Möglichkeit, sich vor Regressionen zu schützen und Fortschritte zu markieren, während Sie den Code einfach halten. Es ist eine Entwicklungsstrategie und keine Teststrategie.
Der Grund, warum ich diesen Unterschied erwähne, ist, dass er Ihnen hilft, die Tests zu bestimmen, die Sie schreiben sollten. Die Antwort auf "Welche Tests soll ich schreiben?" lautet: "Welche Tests auch immer Sie benötigen, um den Code so abzurufen, wie Sie ihn möchten." Stellen Sie sich TDD als eine Möglichkeit vor, Algorithmen und Gründe für Ihren Code herauszufinden. Welcher Test kommt angesichts Ihres Tests und meiner "einfachen grünen" Implementierung als Nächstes? Nun, Sie haben etwas etabliert, das ein volles Haus ist. Wann ist es also kein volles Haus?
Nun müssen Sie einen Weg finden, um zwischen den beiden Testfällen zu unterscheiden, der von Bedeutung ist . Ich persönlich würde ein bisschen klärende Informationen dazu verwenden, "das Einfachste zu tun, um den Test zu bestehen" und zu sagen, "das Einfachste zu tun, um den Test zu bestehen, der Ihre Implementierung fördert". Das Schreiben fehlgeschlagener Tests ist Ihr Vorwand, um den Code zu ändern. Wenn Sie also jeden Test schreiben, sollten Sie sich fragen: "Was macht mein Code nicht, was soll er und wie kann ich diesen Mangel aufdecken?" Es kann Ihnen auch dabei helfen, Ihren Code robust zu machen und Edge Cases zu verarbeiten. Was machst du, wenn ein Anrufer Unsinn eingibt?
Zusammenfassend lässt sich sagen, dass Sie beim Testen jeder Wertekombination mit ziemlicher Sicherheit etwas falsch machen (und wahrscheinlich mit einer kombinatorischen Explosion von Bedingungen enden werden). Wenn es um TDD geht, sollten Sie die Mindestanzahl an Testfällen schreiben, die erforderlich sind, um den gewünschten Algorithmus zu erhalten. Alle weiteren Tests, die Sie schreiben, beginnen grün und werden somit im Wesentlichen zur Dokumentation und sind nicht strikter Bestandteil des TDD-Prozesses. Sie werden nur dann weitere TDD-Testfälle schreiben, wenn sich die Anforderungen ändern oder ein Fehler aufgedeckt wird. In diesem Fall werden Sie den Mangel mit einem Test dokumentieren und ihn dann bestehen lassen.
Aktualisieren:
Ich habe dies als Kommentar zu Ihrem Update begonnen, aber es hat ziemlich lange gedauert ...
Ich würde sagen, das Problem liegt nicht in der Existenz von Literalen, Punkt, sondern darin, dass das 'Einfachste' eine 5-teilige Bedingung ist. Wenn Sie darüber nachdenken, ist eine 5-teilige Bedingung eigentlich ziemlich kompliziert. Es ist üblich, Literale während des Rot-Grün-Schritts zu verwenden und sie dann zu Konstanten im Refactor-Schritt zu abstrahieren oder sie in einem späteren Test zu verallgemeinern.
Während meiner eigenen Reise mit TDD wurde mir klar, dass eine wichtige Unterscheidung getroffen werden muss - es ist nicht gut, "einfach" und "stumpf" zu verwechseln. Das heißt, als ich anfing, sah ich zu, wie Leute TDD machten, und ich dachte, "sie tun nur das Dümmste, was möglich ist, um die Tests zu bestehen", und ahmte das eine Weile nach, bis mir klar wurde, dass "einfach" auf subtile Weise anders war als "stumpf". Manchmal überlappen sie sich, aber oft nicht.
Entschuldigung, wenn ich den Eindruck erweckte, dass die Existenz von Literalen das Problem war - ist es nicht. Ich würde sagen, die Komplexität der Bedingung mit den 5 Sätzen ist das Problem. Ihr erstes Rot zu Grün kann einfach "wahr" sein, weil das wirklich einfach (und zufällig stumpf) ist. Der nächste Testfall mit (1, 2, 3, 4, 5) muss false zurückgeben, und hier beginnt man, "stumpf" zurückzulassen. Sie müssen sich fragen: "Warum ist (1, 1, 1, 2, 2) ein volles Haus und (1, 2, 3, 4, 5) nicht?" Das Einfachste, was Sie sich vorstellen können, ist, dass eines das letzte Sequenzelement 5 oder das zweite Sequenzelement 2 hat und das andere nicht. Diese sind einfach, aber auch (unnötig) stumpf. Was Sie wirklich fahren wollen, ist "wie viele der gleichen Nummer haben sie?" Sie können also den zweiten Test bestehen, indem Sie prüfen, ob eine Wiederholung vorliegt oder nicht. In der einen mit einer Wiederholung hast du ein volles Haus und in der anderen nicht. Jetzt besteht der Test und Sie schreiben einen weiteren Testfall, der eine Wiederholung aufweist, aber kein komplettes Haus ist, um Ihren Algorithmus weiter zu verfeinern.
Sie können dies mit Literalen tun oder auch nicht, und es ist in Ordnung, wenn Sie dies tun. Die allgemeine Idee ist jedoch, Ihren Algorithmus "organisch" zu erweitern, wenn Sie weitere Fälle hinzufügen.
quelle
Das Testen von fünf bestimmten Literalwerten in einer bestimmten Kombination ist für mein fiebriges Gehirn nicht "einfach". Wenn die Lösung für ein Problem wirklich offensichtlich ist (zählen Sie, ob Sie genau drei und genau zwei von einem beliebigen Wert haben), gehen Sie auf jeden Fall vor und codieren Sie diese Lösung und schreiben Sie einige Tests, mit denen Sie sehr, sehr unwahrscheinlich aus Versehen zufrieden sein werden die Menge an Code, die Sie geschrieben haben (dh unterschiedliche Literale und unterschiedliche Ordnungen der Tripel und Doppelten).
TDD-Maximen sind wirklich Werkzeuge, keine religiösen Überzeugungen. Ihr Ziel ist es, Sie dazu zu bringen, schnell korrekten, gut faktorisierten Code zu schreiben. Wenn dem offensichtlich eine Maxime im Wege steht, springen Sie einfach voran und fahren Sie mit dem nächsten Schritt fort. In Ihrem Projekt gibt es viele nicht offensichtliche Stellen, an denen Sie es anwenden können.
quelle
Eriks Antwort ist großartig, aber ich dachte, ich könnte einen Trick beim Testschreiben teilen.
Beginnen Sie mit diesem Test:
Dieser Test wird noch besser, wenn Sie eine
Roll
Klasse erstellen , anstatt 5 Parameter zu übergeben:Das gibt diese Implementierung:
Dann schreibe diesen Test:
Sobald das vorbei ist, schreibe folgendes:
Danach brauchst du bestimmt nicht mehr zu schreiben (vielleicht zwei Paare oder yahtzee, wenn du denkst, dass es kein Full House ist).
Implementieren Sie natürlich Ihre Any-Methoden, um zufällige Rolls zurückzugeben, die Ihren Kriterien entsprechen.
Dieser Ansatz bietet einige Vorteile:
quelle
IsFullHouse
wirklich zurückkehren,true
wennpairNum == trioNum
?Ich kann mir zwei Hauptmethoden vorstellen, die ich beim Testen in Betracht ziehen würde.
Fügen Sie "einige" weitere Testfälle (~ 5) von gültigen Full-House-Sets hinzu, und die gleiche Anzahl von erwarteten Fehlern ({1, 1, 2, 3, 3} ist eine gute. Denken Sie daran, dass es beispielsweise 5 sein könnten als "3 desselben plus ein Paar" durch eine falsche Implementierung erkannt). Bei dieser Methode wird davon ausgegangen, dass der Entwickler nicht nur versucht, die Tests zu bestehen, sondern sie tatsächlich korrekt umzusetzen.
Testen Sie alle möglichen Würfelsätze (es gibt nur 252 verschiedene). Dies setzt natürlich voraus, dass Sie eine Möglichkeit haben zu wissen, wie die erwartete Antwort lautet (beim Testen wird dies als bezeichnet
oracle
). Dies könnte eine Referenzimplementierung derselben Funktion oder ein Mensch sein. Wenn Sie wirklich streng sein möchten, kann es sich lohnen, jedes erwartete Ergebnis manuell zu codieren.Zufällig habe ich einmal eine Yahtzee-KI geschrieben, die natürlich die Regeln kennen musste. Den Code für den Teil zur Bewertung der Punktzahl finden Sie hier . Bitte beachten Sie, dass die Implementierung für die skandinavische Version (Yatzy) vorgesehen ist. Bei unserer Implementierung wird davon ausgegangen, dass die Würfel in sortierter Reihenfolge ausgegeben werden.
quelle
Dieses Beispiel geht wirklich am Rande vorbei. Wir sprechen hier von einer einfachen Funktion, nicht von einem Software-Design. Ist es ein bisschen kompliziert? ja, also machst du es kaputt. Und Sie testen auf keinen Fall jede mögliche Eingabe von 1, 1, 1, 1, 1 bis 6, 6, 6, 6, 6, 6. Die betreffende Funktion erfordert keine Reihenfolge, sondern nur eine Kombination, nämlich AAABB.
Sie benötigen keine 200 separaten Logiktests. Sie könnten zum Beispiel ein Set verwenden. In fast jeder Programmiersprache ist Folgendes integriert:
Und wenn du eine Eingabe erhältst, die kein gültiger Yahtzee-Wurf ist, solltest du werfen, als gäbe es kein Morgen.
quelle