Ich habe eine Klasse, die dazu gedacht ist, ein zufälliges Passwort mit einer Länge zu generieren, die ebenfalls zufällig ist, aber auf eine definierte minimale und maximale Länge begrenzt ist.
Ich erstelle Unit-Tests und bin mit dieser Klasse auf einen interessanten kleinen Haken gestoßen. Die ganze Idee hinter einem Unit-Test ist, dass er wiederholbar sein sollte. Wenn Sie den Test hundert Mal ausführen, sollte er hundert Mal dieselben Ergebnisse liefern. Wenn Sie von einer Ressource abhängig sind, die möglicherweise nicht vorhanden ist oder sich möglicherweise nicht im erwarteten Ausgangszustand befindet, sollten Sie die betreffende Ressource verspotten, um sicherzustellen, dass Ihr Test wirklich immer wiederholbar ist.
Aber was ist mit den Fällen, in denen das SUT eine unbestimmte Ausgabe erzeugen soll?
Wenn ich die minimale und maximale Länge auf den gleichen Wert festlege, kann ich leicht überprüfen, ob das generierte Passwort die erwartete Länge hat. Wenn ich jedoch einen Bereich zulässiger Längen (z. B. 15 bis 20 Zeichen) spezifiziere, haben Sie jetzt das Problem, dass Sie den Test hundertmal ausführen und 100 Durchgänge erhalten könnten, aber beim 101. Durchgang erhalten Sie möglicherweise eine 9-Zeichen-Zeichenfolge zurück.
Bei der Kennwortklasse, die im Kern recht einfach ist, sollte sich dies nicht als großes Problem herausstellen. Aber ich habe über den allgemeinen Fall nachgedacht. Welche Strategie wird normalerweise als die beste angenommen, wenn es um SUTs geht, die vom Design her eine unbestimmte Ausgabe generieren?
quelle
Antworten:
"Nicht-deterministische" Ausgaben sollten für die Zwecke des Komponententests deterministisch werden können. Eine Möglichkeit, mit Zufälligkeiten umzugehen, besteht darin, das Ersetzen der Zufallsmaschine zu ermöglichen. Hier ist ein Beispiel (PHP 5.3+):
Sie können eine spezielle Testversion der Funktion erstellen, die eine beliebige Folge von Zahlen zurückgibt, um sicherzustellen, dass der Test vollständig wiederholbar ist. Im realen Programm können Sie eine Standardimplementierung haben, die das Fallback sein kann, wenn sie nicht überschrieben wird.
quelle
Das tatsächliche Ausgabekennwort ist möglicherweise nicht bei jeder Ausführung der Methode festgelegt, verfügt jedoch über bestimmte Funktionen, die getestet werden können, z. B. Mindestlänge, Zeichen in einem bestimmten Zeichensatz usw.
Sie können auch testen, ob die Routine jedes Mal ein bestimmtes Ergebnis zurückgibt, indem Sie Ihren Passwortgenerator jedes Mal mit demselben Wert ausstatten.
quelle
Test gegen "den Vertrag". Wenn die Methode als "generiert Passwörter mit einer Länge von 15 bis 20 Zeichen mit az" definiert ist, testen Sie sie auf diese Weise
Zusätzlich können Sie die Generation extrahieren, damit alles, was darauf beruht, mit einer anderen "statischen" Generatorklasse getestet werden kann
quelle
Sie haben eine
Password generator
und Sie brauchen eine zufällige Quelle.Wie Sie in der Frage angegeben haben, gibt a
random
eine nicht deterministische Ausgabe aus, da es sich um einen globalen Zustand handelt . Das heißt, es greift auf etwas außerhalb des Systems zu, um Werte zu generieren.Sie können so etwas nie für alle Ihre Klassen loswerden, aber Sie können die Kennwortgenerierung für die Erstellung von Zufallswerten trennen.
Wenn Sie den Code wie folgt strukturieren, können Sie das
RandomSource
für Ihre Tests verspotten .Sie können das nicht zu 100% testen,
RandomSource
aber die Vorschläge, die Sie zum Testen der Werte in dieser Frage erhalten haben, können darauf angewendet werden (wie beim Testen, bei demrand->(1,26);
immer eine Zahl von 1 bis 26 zurückgegeben wird).quelle
Im Fall einer Teilchenphysik Monte Carlo habe ich "Unit Tests" {*} geschrieben, die die nicht deterministische Routine mit einem voreingestellten Zufallskeim aufrufen , dann eine statistische Anzahl von Malen ausführen und auf Verstöße gegen Beschränkungen (Energieniveaus) prüfen oberhalb der eingegebenen Energie muss unzugänglich sein, alle Durchläufe müssen eine bestimmte Stufe auswählen, usw.) und Regressionen gegenüber den zuvor aufgezeichneten Ergebnissen.
{*} Ein solcher Test verstößt gegen das Prinzip "Test schnell machen" für Komponententests, sodass Sie sich vielleicht besser fühlen, wenn Sie sie auf andere Weise charakterisieren: beispielsweise Akzeptanztests oder Regressionstests. Trotzdem habe ich mein Unit-Testing-Framework verwendet.
quelle
Ich muss der akzeptierten Antwort aus zwei Gründen widersprechen :
(Beachten Sie, dass es unter vielen Umständen eine gute Antwort sein kann, aber nicht in allen und vielleicht auch nicht in den meisten.)
Was meine ich damit? Nun, mit Überanpassung meine ich ein typisches Problem des statistischen Testens: Überanpassung tritt auf, wenn Sie einen stochastischen Algorithmus gegen einen übermäßig eingeschränkten Datensatz testen. Wenn Sie dann zurückgehen und Ihren Algorithmus verfeinern, passen Sie ihn implizit sehr gut an die Trainingsdaten an (Sie passen Ihren Algorithmus versehentlich an die Testdaten an), aber alle anderen Daten passen möglicherweise überhaupt nicht an (weil Sie nie dagegen testen). .
(Im Übrigen ist dies immer ein Problem, das beim Testen von Einheiten auftritt. Aus diesem Grund sind gute Tests vollständig oder zumindest repräsentativ für eine bestimmte Einheit, und dies ist im Allgemeinen schwierig.)
Wenn Sie Ihre Tests deterministisch machen, indem Sie den Zufallszahlengenerator steckbar machen, testen Sie immer mit demselben sehr kleinen und (normalerweise) nicht repräsentativen Datensatz. Dies verzerrt Ihre Daten und kann zu Verzerrungen in Ihrer Funktion führen.
Der zweite Punkt, Unpraktikabilität, entsteht, wenn Sie keine Kontrolle über die stochastische Variable haben. Dies passiert normalerweise nicht mit Zufallszahlengeneratoren (es sei denn, Sie benötigen eine „echte“ Zufallsquelle), aber es kann passieren, dass sich Stochastiken auf andere Weise in Ihr Problem einschleichen. Wenn Sie beispielsweise gleichzeitigen Code testen: Race-Bedingungen sind immer stochastisch, können Sie sie nicht (einfach) deterministisch machen.
Die einzige Möglichkeit, das Vertrauen in diesen Fällen zu stärken, besteht darin, viel zu testen . Aufschäumen, ausspülen, wiederholen. Dies erhöht das Vertrauen bis zu einem gewissen Grad (zu diesem Zeitpunkt wird der Kompromiss für zusätzliche Testläufe vernachlässigbar).
quelle
Sie haben hier tatsächlich mehrere Verantwortlichkeiten. Unit-Tests und insbesondere TDD eignen sich hervorragend, um solche Dinge hervorzuheben.
Verantwortlichkeiten sind:
1) Zufallszahlengenerator. 2) Passwortformatierer.
Der Passwortformatierer verwendet den Zufallszahlengenerator. Fügen Sie den Generator über seinen Konstruktor als Schnittstelle in Ihren Formatierer ein. Jetzt können Sie Ihren Zufallszahlengenerator vollständig testen (statistischer Test) und den Formatierer durch Injizieren eines verspotteten Zufallszahlengenerators testen.
Sie erhalten nicht nur besseren Code, sondern auch bessere Tests.
quelle
Wie die anderen bereits erwähnt haben, testen Sie diesen Code, indem Sie die Zufälligkeit entfernen.
Möglicherweise möchten Sie auch einen übergeordneten Test durchführen, der den Zufallszahlengenerator aktiviert lässt, nur den Vertrag testet (Kennwortlänge, zulässige Zeichen, ...) und im Fehlerfall genügend Informationen ausgibt, damit Sie das System reproduzieren können Zustand in der einen Instanz, in der der Zufallstest fehlgeschlagen ist.
Es spielt keine Rolle, dass der Test selbst nicht wiederholbar ist - solange Sie den Grund finden, warum er dieses Mal fehlgeschlagen ist.
quelle
Viele Unit-Test-Schwierigkeiten werden trivial, wenn Sie Ihren Code umgestalten, um Abhängigkeiten zu beseitigen. Eine Datenbank, ein Dateisystem, der Benutzer oder in Ihrem Fall eine Zufallsquelle.
Eine andere Betrachtungsweise ist, dass Unit-Tests die Frage beantworten sollen, ob dieser Code das tut, was ich beabsichtige. In Ihrem Fall wissen Sie nicht, was der Code tun soll, da er nicht deterministisch ist.
Teilen Sie in diesem Sinne Ihre Logik in kleine, leicht verständliche, leicht zu testende Einzelteile auf. Insbesondere erstellen Sie eine bestimmte Methode (oder Klasse!), Die eine Zufallsquelle als Eingabe verwendet und das Kennwort als Ausgabe erstellt. Dieser Code ist eindeutig deterministisch.
In Ihrem Komponententest geben Sie jedes Mal dieselbe nicht ganz zufällige Eingabe ein. Codieren Sie für sehr kleine Zufallsströme die Werte in Ihrem Test einfach hart. Andernfalls geben Sie dem RNG in Ihrem Test einen konstanten Startwert.
Bei einer höheren Teststufe (nennen Sie es "Akzeptanz" oder "Integration" oder was auch immer) lassen Sie den Code mit einer echten Zufallsquelle laufen.
quelle
Die meisten der obigen Antworten weisen darauf hin, dass das Verspotten des Zufallszahlengenerators der richtige Weg ist, ich habe jedoch einfach die eingebaute mt_rand-Funktion verwendet. Das Ermöglichen des Verspottens hätte bedeutet, dass die Klasse neu geschrieben werden musste, damit zur Konstruktionszeit ein Zufallszahlengenerator eingefügt werden musste.
Zumindest dachte ich das!
Eine der Konsequenzen des Hinzufügens von Namespaces ist, dass das in PHP-Funktionen integrierte Mocking von unglaublich schwer zu trivial einfach geworden ist. Befindet sich das SUT in einem bestimmten Namespace, müssen Sie im Unit-Test unter diesem Namespace lediglich Ihre eigene mt_rand-Funktion definieren. Diese wird für die Dauer des Tests anstelle der integrierten PHP-Funktion verwendet.
Hier ist die endgültige Testsuite:
Ich dachte, ich würde das erwähnen, weil das Überschreiben von PHP-internen Funktionen eine andere Verwendung für Namespaces ist, die mir einfach nicht eingefallen sind. Vielen Dank an alle für die Hilfe.
quelle
In dieser Situation sollte ein zusätzlicher Test durchgeführt werden, um sicherzustellen, dass wiederholte Aufrufe des Kennwortgenerators tatsächlich unterschiedliche Kennwörter erzeugen. Wenn Sie einen thread-sicheren Kennwortgenerator benötigen, sollten Sie auch gleichzeitige Aufrufe mit mehreren Threads testen.
Dies stellt im Wesentlichen sicher, dass Sie Ihre Zufallsfunktion ordnungsgemäß verwenden und nicht bei jedem Anruf neu festlegen.
quelle