Was sind gute Unit-Tests für den Anwendungsfall des Würfelns?

18

Ich versuche, mit Unit-Tests klarzukommen.

Angenommen, wir haben einen Würfel, der eine Standardanzahl von Seiten von 6 hat (kann aber 4, 5 Seiten usw. haben):

import random
class Die():
    def __init__(self, sides=6):
        self._sides = sides

    def roll(self):
        return random.randint(1, self._sides)

Wäre das Folgende ein gültiger / nützlicher Komponententest?

  • Testen Sie eine Rolle im Bereich von 1 bis 6 für einen 6-seitigen Würfel
  • teste einen Wurf von 0 für einen 6-seitigen Würfel
  • Teste einen Wurf von 7 für einen 6-seitigen Würfel
  • Testen Sie eine Rolle im Bereich 1-3 für einen dreiseitigen Würfel
  • teste einen Wurf von 0 für einen dreiseitigen Würfel
  • Testen Sie eine Rolle mit 4 für einen dreiseitigen Würfel

Ich denke nur, dass dies eine Zeitverschwendung ist, da es das Zufallsmodul schon lange genug gibt, aber dann denke ich, wenn das Zufallsmodul aktualisiert wird (sagen wir, ich aktualisiere meine Python-Version), bin ich zumindest darüber informiert.

Muss ich in diesem Fall auch noch andere Variationen von Würfeln testen, z. B. die 3, oder ist es gut, einen anderen initialisierten Würfelzustand abzudecken?

Cybran
quelle
1
Was ist mit einem Minus-5-seitigen Würfel oder einem Null-seitigen Würfel?
JensG

Antworten:

22

Sie haben Recht, Ihre Tests sollten nicht bestätigen, dass das randomModul seine Aufgabe erfüllt. Ein Unittest sollte nur die Klasse selbst testen, nicht die Interaktion mit anderem Code (der separat getestet werden sollte).

Es ist natürlich durchaus möglich, dass Ihr Code random.randint()falsch verwendet; oder du callst random.randrange(1, self._sides)stattdessen und dein Würfel wirft nie den höchsten Wert, aber das wäre eine andere Art von Bug, die du nicht mit einem Unittest fangen könntest. In diesem Fall funktioniert Ihr die Gerät wie geplant, aber das Design selbst war fehlerhaft.

In diesem Fall würde ich die Funktion durch Verspotten ersetzenrandint() und nur überprüfen, ob sie korrekt aufgerufen wurde . Python 3.3 und höher enthält das unittest.mockModul für diese Art von Tests. Sie können das externe mockPaket jedoch auch auf älteren Versionen installieren , um genau die gleiche Funktionalität zu erhalten

import unittest
try:
    from unittest.mock import patch
except ImportError:
    # < python 3.3
    from mock import patch


@patch('random.randint', return_value=3)
class TestDice(unittest.TestCase):
    def _make_one(self, *args, **kw):
        from die import Die
        return Die(*args, **kw)

    def test_standard_size(self, mocked_randint):
        die = self._make_one()
        result = die.roll()

        mocked_randint.assert_called_with(1, 6)
        self.assertEqual(result, 3)

    def test_custom_size(self, mocked_randint):
        die = self._make_one(sides=42)
        result = die.roll()

        mocked_randint.assert_called_with(1, 42)
        self.assertEqual(result, 3)


if __name__ == '__main__':
    unittest.main()

Mit dem Verspotten ist Ihr Test jetzt sehr einfach; es gibt eigentlich nur 2 fälle. Der Standardfall für einen 6-seitigen Würfel und der benutzerdefinierte Seitenfall.

Es gibt andere Möglichkeiten, die randint()Funktion im globalen Namespace von vorübergehend zu ersetzen Die, aber das mockModul macht dies am einfachsten. Der @mock.patchDekorateur gilt hier für alle Testmethoden im Testfall; Jeder Testmethode wird ein zusätzliches Argument übergeben, die verspottete random.randint()Funktion, damit wir anhand der Verspottung testen können, ob sie tatsächlich korrekt aufgerufen wurde. Das return_valueArgument gibt an, was vom Mock zurückgegeben wird, wenn es aufgerufen wird, sodass wir überprüfen können, ob die die.roll()Methode tatsächlich das "zufällige" Ergebnis an uns zurückgegeben hat.

Ich habe hier eine andere, nicht zu testende, bewährte Methode von Python verwendet: Importieren Sie die zu testende Klasse als Teil des Tests. Das _make_oneVerfahren hat den Import und Instanziierung Arbeit in einem Test , so dass das Testmodul noch selbst wird geladen , wenn Sie einen Syntaxfehler oder andere Fehler gemacht, die das ursprüngliche Modul Import verhindern werde.

Wenn Sie auf diese Weise einen Fehler im Modulcode selbst gemacht haben, werden die Tests weiterhin ausgeführt. Sie werden nur scheitern und Sie über den Fehler in Ihrem Code informieren.

Die obigen Tests sind im Extremfall einfach. Das Ziel ist hier nicht zu testen random.randint(), was zum Beispiel mit den richtigen Argumenten aufgerufen wurde. Stattdessen soll getestet werden, ob das Gerät bei bestimmten Eingaben die richtigen Ergebnisse liefert, wobei diese Eingaben die Ergebnisse anderer nicht getesteter Geräte einschließen . Durch das Verspotten der random.randint()Methode können Sie nur eine weitere Eingabe in Ihren Code steuern.

In realen Tests wird der tatsächliche Code in Ihrem zu testenden Gerät komplexer. Die Beziehung zu Eingaben, die an die API übergeben werden, und wie andere Einheiten dann aufgerufen werden, kann immer noch interessant sein. Durch das Verspotten erhalten Sie Zugriff auf Zwischenergebnisse und können die Rückgabewerte für diese Aufrufe festlegen.

In Code, der Benutzer für einen OAuth2-Dienst eines Drittanbieters authentifiziert (eine mehrstufige Interaktion), möchten Sie beispielsweise testen, ob Ihr Code die richtigen Daten an diesen Dienst eines Drittanbieters weiterleitet, und Sie können verschiedene Fehlerantworten ausspotten Der Dienst eines Drittanbieters würde zurückkehren und Sie könnten verschiedene Szenarien simulieren, ohne selbst einen vollständigen OAuth2-Server erstellen zu müssen. Hier ist es wichtig zu testen, ob die Informationen aus einer ersten Antwort korrekt verarbeitet und an einen zweiten Aufruf weitergeleitet wurden. Sie möchten also sicherstellen, dass der gespielte Dienst korrekt aufgerufen wird.

Martijn Pieters
quelle
1
Sie haben einige mehr als 2 Testfälle ... Ergebnisse überprüfen auf Standardwerte: Unter (1), Ober (6), Unter (0), Über (7) und Ergebnisse für benutzerdefinierte Zahlen wie max_int usw. Eingabe ist auch nicht validiert, was möglicherweise irgendwann getestet werden muss ...
James Snell
2
Nein, das sind Tests randint(), nicht der Code in Die.roll().
Martijn Pieters
Es gibt tatsächlich eine Möglichkeit, um sicherzustellen, dass nicht nur randint richtig aufgerufen wird, sondern auch das Ergebnis richtig verwendet wird: Verspotten Sie es, sentinel.dieum beispielsweise ein Sentinel-Objekt zurückzugeben (Sentinel-Objekt stammt von unittest.mockzu), und überprüfen Sie dann, ob es das ist, was von Ihrer Roll-Methode zurückgegeben wurde. Dies ermöglicht tatsächlich nur einen Weg, die getestete Methode zu implementieren.
Aragaer
@aragaer: Wenn Sie sicherstellen möchten, dass der Wert unverändert zurückgegeben wird, können Sie sentinel.diedies auf hervorragende Weise sicherstellen.
Martijn Pieters
Ich verstehe nicht, warum Sie sicherstellen möchten, dass mocked_randint mit bestimmten Werten aufgerufen wird. Ich verstehe, Randint verspotten zu wollen, um vorhersagbare Werte zurückzugeben, aber ist es nicht das Problem, dass es vorhersagbare Werte zurückgibt und nicht, mit welchen Werten es aufgerufen wird? Es scheint mir, dass das Überprüfen der aufgerufenen Werte den Test unnötig mit feinen Details der Implementierung verknüpft. Auch warum ist es uns wichtig, dass der Würfel den exakten Wert von randint zurückgibt? Interessiert es uns nicht wirklich nur, dass es einen Wert> 1 und kleiner als das Maximum zurückgibt?
bdrx
16

Martijns Antwort lautet, wie Sie es machen würden, wenn Sie wirklich einen Test durchführen möchten, der zeigt, dass Sie random.randint aufrufen. Auf die Gefahr angesprochen zu werden, dass "das die Frage nicht beantwortet", sollte dies meines Erachtens überhaupt nicht auf die Einheit getestet werden. Randint zu verspotten ist kein Black-Box-Test mehr - Sie zeigen ausdrücklich, dass bestimmte Dinge in der Implementierung vor sich gehen . Black-Box-Tests sind noch nicht einmal eine Option - es gibt keinen Test, den Sie ausführen können, um zu beweisen, dass das Ergebnis niemals unter 1 oder über 6 liegen wird.

Kannst du dich lustig machen randint? Ja, du kannst. Aber was beweist du? Dass Sie es mit Argumenten 1 und Seiten bezeichnet haben. Was bedeutet , dass Mittel? Sie sind wieder auf dem ersten Platz - am Ende des Tages müssen Sie formal oder informell nachweisen, dass random.randint(1, sides)ein Würfelwurf korrekt ausgeführt wird.

Ich bin alle für Unit-Tests. Sie sind fantastische Gesundheitskontrollen und decken das Vorhandensein von Fehlern auf. Sie können jedoch niemals ihre Abwesenheit beweisen, und es gibt Dinge, die durch Tests überhaupt nicht bestätigt werden können (z. B. dass eine bestimmte Funktion niemals eine Ausnahme auslöst oder immer endet) gewinnen. Für deterministisches Verhalten sind Komponententests sinnvoll, da Sie tatsächlich wissen, welche Antwort Sie erwarten.

Doval
quelle
Unit-Tests sind eigentlich keine Black-Box-Tests. Dafür sind Integrationstests gedacht, um sicherzustellen, dass die verschiedenen Teile wie geplant interagieren. Es ist natürlich eine Ansichtssache (die meisten Testphilosophien), siehe Fällt "Unit Testing" unter White-Box- oder Black-Box-Tests? und Black Box Unit Testing für einige Perspektiven (Stack Overflow).
Martijn Pieters
@MartijnPieters Ich bin nicht einverstanden, dass "das ist, was Integrationstests sind". Integrationstests dienen zur Überprüfung der korrekten Interaktion aller Systemkomponenten. Sie sind nicht der Ort, um zu testen, ob eine bestimmte Komponente für eine bestimmte Eingabe die richtige Ausgabe liefert. In Bezug auf Black-Box- und White-Box-Unit-Tests brechen White-Box-Unit-Tests möglicherweise mit Implementierungsänderungen und alle Annahmen, die Sie in der Implementierung getroffen haben, werden sich wahrscheinlich auf den Test übertragen. Die Validierung, random.randintmit der aufgerufen wird, 1, sidesist wertlos, wenn dies falsch ist.
Doval
Ja, das ist eine Einschränkung eines White-Box-Unit-Tests. Es macht jedoch keinen Sinn zu testen, ob random.randint()Werte im Bereich [1, Seiten] (einschließlich) randomkorrekt zurückgegeben werden. Es liegt an den Python-Entwicklern, sicherzustellen, dass das Gerät ordnungsgemäß funktioniert.
Martijn Pieters
Und wie Sie selbst sagen, können Unit-Tests nicht garantieren, dass Ihr Code fehlerfrei ist. Wenn Ihr Code andere Einheiten falsch verwendet (sagen wir, Sie haben erwartet, dass Sie random.randint()sich so verhalten, random.randrange()und nennen es random.randint(1, sides + 1)so), sind Sie trotzdem gesunken.
Martijn Pieters
2
@MartijnPieters Da stimme ich Ihnen zu, aber ich bin nicht dagegen. Ich lehne es ab zu testen, dass random.randint mit Argumenten (1, Seiten) aufgerufen wird . Sie haben in der Implementierung davon ausgegangen, dass dies die richtige Vorgehensweise ist, und wiederholen diese Annahme jetzt im Test. Sollte diese Annahme falsch sein, ist der Test bestanden, aber Ihre Implementierung ist immer noch falsch. Es ist ein halbherziger Beweis, den man schreiben und pflegen muss.
Doval
6

Fix zufällige Samen. Vergewissern Sie sich bei 1, 2, 5 und 12-seitigen Würfeln, dass einige Tausend Würfe Ergebnisse einschließlich 1 und N und nicht einschließlich 0 oder N + 1 liefern decken Sie den erwarteten Bereich ab, wechseln Sie zu einem anderen Samen.

Spottwerkzeuge sind cool, aber nur weil man damit etwas machen kann, heißt das noch lange nicht, dass etwas gemacht werden sollte. YAGNI gilt für Testvorrichtungen ebenso wie für Funktionen.

Wenn Sie problemlos mit nicht gespielten Abhängigkeiten testen können, sollten Sie dies so ziemlich immer tun. Auf diese Weise konzentrieren sich Ihre Tests auf die Reduzierung der Fehleranzahl und nicht nur auf die Erhöhung der Testanzahl. Übermäßiges Verspotten kann zu irreführenden Berichterstattungszahlen führen, was wiederum dazu führen kann, dass die eigentlichen Tests auf eine spätere Phase verschoben werden. Vielleicht haben Sie nie Zeit, sich mit ...

Soru
quelle
3

Was ist ein, Diewenn Sie darüber nachdenken? - nicht mehr als ein Wrapper herum random. Sie kapselt random.randintund neu etikettiert , es in Bezug auf die Anwendung des eigenen Wortschatzes: Die.Roll.

Ich finde es nicht relevant, eine weitere Abstraktionsebene zwischen Dieund einzufügen, randomda es Diesich bereits um diese Indirektionsebene zwischen Ihrer Anwendung und der Plattform handelt.

Wenn Sie Dosenwürfelergebnisse wünschen, verspottenDierandom Sie einfach, verspotten Sie nicht .

Im Allgemeinen teste ich meine Wrapper-Objekte, die mit externen Systemen kommunizieren, nicht nach Einheiten, sondern schreibe Integrationstests für sie. Sie könnten ein paar davon schreiben, Dieaber wie Sie betont haben, sind sie aufgrund der Zufälligkeit des zugrunde liegenden Objekts nicht aussagekräftig. Außerdem sind hier keine Konfiguration oder Netzwerkkommunikation erforderlich, sodass außer einem Plattformaufruf nicht viel zu testen ist.

=> In Anbetracht der Tatsache, dass dies Dienur ein paar triviale Codezeilen sind und im Vergleich zu sich randomselbst wenig oder gar keine Logik hinzufügen , würde ich das Testen in diesem speziellen Beispiel überspringen.

guillaume31
quelle
2

Das Setzen des Zufallszahlengenerators und das Überprüfen der erwarteten Ergebnisse ist meines Erachtens KEIN gültiger Test. Es wird davon ausgegangen, WIE Ihre Würfel intern funktionieren, was sehr ungezogen ist. Die Entwickler von Python könnten den Zufallszahlengenerator ändern oder den Würfel (HINWEIS: "Würfel" ist Plural, "Würfel" ist Singular. Wenn Ihre Klasse nicht mehrere Würfelwürfe in einem Aufruf implementiert, sollte sie wahrscheinlich "Würfel" heißen) Verwenden Sie einen anderen Zufallsgenerator.

Ebenso wird beim Verspotten der Zufallsfunktion davon ausgegangen, dass die Klassenimplementierung genau wie erwartet funktioniert. Warum könnte dies nicht der Fall sein? Jemand übernimmt möglicherweise die Kontrolle über den Standard-Python-Zufallszahlengenerator. Um dies zu vermeiden, ruft eine zukünftige Version Ihres Chips möglicherweise mehrere Zufallszahlen oder größere Zufallszahlen ab, um mehr Zufallsdaten einzumischen. Ein ähnliches Schema wurde von den Herstellern des FreeBSD-Betriebssystems verwendet, als sie den Verdacht hatten, dass der NSA die in CPUs eingebauten Hardware-Zufallszahlengeneratoren manipuliert.

Wenn ich es wäre, würde ich beispielsweise 6000 Rollen ausführen, sie zählen und sicherstellen, dass jede Zahl von 1 bis 6 500 bis 1500 Mal gewürfelt wird. Ich würde auch überprüfen, dass keine Zahlen außerhalb dieses Bereichs zurückgegeben werden. Ich könnte auch überprüfen, ob bei einem zweiten Satz von 6000 Rollen beim Bestellen von [1..6] in der Reihenfolge der Häufigkeit das Ergebnis anders ist (dies schlägt fehl, wenn die Zahlen zufällig sind!). Wenn Sie gründlich sein möchten, finden Sie möglicherweise die Häufigkeit von Zahlen nach einer 1, nach einer 2 usw .; Stellen Sie jedoch sicher, dass Ihre Stichprobengröße groß genug ist und Sie genügend Varianz haben. Menschen erwarten, dass Zufallszahlen weniger Muster haben als sie tatsächlich haben.

Wiederholen Sie diesen Vorgang für einen 12-seitigen und einen 2-seitigen Würfel (6 wird am häufigsten verwendet, so dass dies am meisten von jedem erwartet wird, der diesen Code schreibt).

Schließlich würde ich testen, um zu sehen, was mit einem einseitigen Würfel, einem 0-seitigen Würfel, einem -1-seitigen Würfel, einem 2,3-seitigen Würfel, einem [1,2,3,4,5,6] -seitigen Würfel und passiert ein "bla" -seitiger Würfel. Natürlich sollten diese alle scheitern; Scheitern sie auf nützliche Weise? Diese sollten wahrscheinlich bei der Erstellung scheitern, nicht beim Rollen.

Oder vielleicht möchten Sie auch anders damit umgehen - vielleicht sollte es akzeptabel sein, einen Würfel mit [1,2,3,4,5,6] zu erstellen - und vielleicht auch "blah"; Dies könnte ein Würfel mit 4 Gesichtern sein, auf jedem Gesicht befindet sich ein Buchstabe. Das Spiel "Boggle" kommt einem in den Sinn, ebenso wie eine magische Acht.

Und schließlich möchten Sie vielleicht Folgendes in Betracht ziehen: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg

AMADANON Inc.
quelle
2

Aufgrund der Gefahr, gegen den Strom zu schwimmen, habe ich dieses Problem vor einigen Jahren mit einer bisher nicht erwähnten Methode gelöst.

Meine Strategie bestand einfach darin, das RNG mit einem zu verspotten, der einen vorhersehbaren Strom von Werten erzeugt, der sich über den gesamten Raum erstreckt. Wenn (say) side = 6 ist und das RNG der Reihe nach Werte von 0 bis 5 erzeugt, kann ich vorhersagen, wie sich meine Klasse verhalten soll, und einen entsprechenden Komponententest durchführen.

Der Grund dafür ist, dass dies die Logik in dieser Klasse allein testet, unter der Annahme, dass der RNG schließlich jeden dieser Werte erzeugt, und ohne den RNG selbst zu testen.

Es ist einfach, deterministisch, reproduzierbar und es erkennt Fehler. Ich würde die gleiche Strategie wieder anwenden.


Die Frage legt nicht fest, wie die Tests aussehen sollten, sondern welche Daten bei Vorhandensein eines RNG für die Tests verwendet werden könnten. Mein Vorschlag ist lediglich, ausführlich zu testen, indem ich mich über das RNG lustig mache. Die Frage, was einen Test wert ist, hängt von Informationen ab, die in der Frage nicht enthalten sind.

david.pfx
quelle
Sagen Sie, Sie verspotten den RNG, um vorhersehbar zu sein. Na was testest du dann? Die Frage lautet: "Wären die folgenden Unit-Tests gültig / nützlich?" Das Verspotten, um 0-5 zurückzugeben, ist kein Test, sondern ein Testaufbau. Wie würden Sie "Unit-Test entsprechend"? Ich verstehe nicht, wie es "Fehler fängt". Es fällt mir schwer zu verstehen, was ich für einen Unit-Test benötige.
bdrx
@bdrx: Das war vor einer Weile: Ich würde es jetzt anders beantworten. Aber siehe bearbeiten.
david.pfx
1

Die Tests, die Sie in Ihrer Frage vorschlagen, erkennen keinen modularen arithmetischen Zähler als Implementierung. Und sie erkennen keine häufigen Implementierungsfehler in wahrscheinlichkeitsverteilungsbezogenem Code wie return 1 + (random.randint(1,maxint) % sides). Oder eine Änderung am Generator, die zu zweidimensionalen Mustern führt.

Wenn Sie tatsächlich sicherstellen möchten, dass Sie gleichmäßig verteilte zufällig auftauchende Zahlen generieren, müssen Sie eine Vielzahl von Eigenschaften überprüfen. Um einen einigermaßen guten Job zu machen, könnten Sie http://www.phy.duke.edu/~rgb/General/dieharder.php für Ihre generierten Zahlen ausführen . Oder schreiben Sie eine ähnlich komplexe Unit-Test-Suite.

Das ist nicht die Schuld von Unit-Testing oder TDD, Zufälligkeit ist einfach eine sehr schwer zu überprüfende Eigenschaft. Und ein beliebtes Thema für Beispiele.

Patrick
quelle
-1

Der einfachste Test für einen Würfelwurf besteht darin, ihn mehrere hunderttausend Mal zu wiederholen und zu bestätigen, dass jedes mögliche Ergebnis ungefähr (1 / Anzahl der Seiten) Mal getroffen wurde. Bei einem 6-seitigen Würfel sollte jeder mögliche Wert in etwa 16,6% der Fälle erreicht werden. Wenn einige um mehr als ein Prozent niedriger sind, haben Sie ein Problem.

Auf diese Weise vermeiden Sie, dass Sie den zugrunde liegenden Mechanismus der Generierung einer Zufallszahl einfach und vor allem ohne Änderung des Tests umgestalten können.

ChristopherBrown
quelle
1
Dieser Test wäre für eine völlig nicht-zufällige Umsetzung übergeben , die einfach durch Seiten einer nach der anderen in einer vordefinierten Reihenfolge loops
gnat
1
Wenn ein Codierer beabsichtigt, etwas in böser Absicht zu implementieren (kein Randomisierungsmittel auf einem Würfel zu verwenden) und einfach zu versuchen, etwas zu finden, um die roten Lichter grün werden zu lassen, gibt es mehr Probleme, als durch Unit-Tests wirklich gelöst werden können.
ChristopherBrown