Unit-Test-Methoden mit unbestimmter Ausgabe

37

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?

GordonM
quelle
9
Warum die engen Abstimmungen? Ich denke, dass es eine vollkommen berechtigte Frage ist.
Mark Baker
Huh, danke für den Kommentar. Ich habe das nicht einmal bemerkt, aber jetzt wundere ich mich über das Gleiche. Das Einzige, woran ich denken könnte, ist, dass es sich eher um einen allgemeinen als um einen bestimmten Fall handelt. Ich könnte jedoch einfach die Quelle für die oben genannte Kennwortklasse posten und fragen: "Wie teste ich diese Klasse?" statt "Wie teste ich eine unbestimmte Klasse?"
GordonM
1
@MarkBaker Weil die meisten der unwichtigsten Fragen sich auf programers.se beziehen. Es ist ein Votum für Migration, nicht um die Frage abzuschließen.
Ikke

Antworten:

20

"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+):

function DoSomethingRandom($getRandomIntLessThan)
{
    if ($getRandomIntLessThan(2) == 0)
    {
        // Do action 1
    }
    else
    {
        // Do action 2
    }
}

// For testing purposes, always return 1
$alwaysReturnsOne = function($n) { return 1; };
DoSomethingRandom($alwaysReturnsOne);

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.

bobbymcr
quelle
1
Alle Antworten hatten gute Vorschläge, die ich verwendet habe, aber ich denke, dies ist das Hauptproblem, damit es akzeptiert wird.
GordonM
1
Ziemlich viel Nägel es auf den Kopf. Obwohl nicht deterministisch, gibt es immer noch Grenzen.
Surfasb
21

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.

Mark Baker
quelle
Die PW-Klasse verwaltet eine Konstante, die im Wesentlichen dem Zeichenpool entspricht, aus dem das Kennwort generiert werden soll. Indem ich es unterordnete und die Konstante mit einem einzelnen Zeichen überschrieb, schaffte ich es, einen Bereich der Nichtbestimmtheit für Testzwecke zu beseitigen. So danke.
GordonM
14

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

$this->assertTrue ((bool) preg_match('^[a-z]{15,20}$', $password));

Zusätzlich können Sie die Generation extrahieren, damit alles, was darauf beruht, mit einer anderen "statischen" Generatorklasse getestet werden kann

class RandomGenerator implements PasswordGenerator {
  public function create() {
    // Create $rndPwd
    return $rndPwd;
  }
}

class StaticGenerator implements PasswordGenerator {
  private $pwd;
  public function __construct ($pwd) { $this->pwd = $pwd; }
  public function create      ()     { return $this->pwd; }
}
KingCrunch
quelle
Der Regex, den Sie gaben, erwies sich als nützlich, so dass ich eine optimierte Version in meinen Test einbezog. Vielen Dank.
GordonM
6

Sie haben eine Password generatorund Sie brauchen eine zufällige Quelle.

Wie Sie in der Frage angegeben haben, gibt a randomeine 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.

<?php
class PasswordGenerator {

    public function __construct(RandomSource $randomSource) {
        $this->randomSource = $randomSource
    }

    public function generatePassword() {
        $password = '';
        for($length = rand(10, 16); $length; $length--) {
            $password .= $this-toChar($this->randomSource->rand(1,26));
        }
    }

}

Wenn Sie den Code wie folgt strukturieren, können Sie das RandomSourcefür Ihre Tests verspotten .

Sie können das nicht zu 100% testen, RandomSourceaber die Vorschläge, die Sie zum Testen der Werte in dieser Frage erhalten haben, können darauf angewendet werden (wie beim Testen, bei dem rand->(1,26);immer eine Zahl von 1 bis 26 zurückgegeben wird).

edorian
quelle
Das ist eine großartige Antwort.
Nick Hodges
3

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.

dmckee
quelle
3

Ich muss der akzeptierten Antwort aus zwei Gründen widersprechen :

  1. Überanpassung
  2. Unpraktikabilität

(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).

Konrad Rudolph
quelle
2

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.

Rob Smyth
quelle
2

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.

Simon Richter
quelle
2

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.

Jay Bazuzi
quelle
Diese Antwort hat mich überzeugt: Ich hatte wirklich zwei Funktionen in einer: den Zufallszahlengenerator und die Funktion, die etwas mit dieser Zufallszahl gemacht hat. Ich habe es einfach überarbeitet und kann jetzt problemlos den nicht deterministischen Teil des Codes testen und die vom Zufallsteil generierten Parameter eingeben. Das Schöne ist, dass ich dann in meinem Unit-Test festgelegte Parameter (verschiedene Sätze) eingeben kann (ich verwende einen Zufallszahlengenerator aus der Standardbibliothek, also teste ich das sowieso nicht).
Neuronet
1

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:

namespace gordian\reefknot\util;

/**
 * The following function will take the place of mt_rand for the duration of 
 * the test.  It always returns the number exactly half way between the min 
 * and the max.
 */
function mt_rand ($min = 42, $max = NULL)
{
    $min    = intval ($min);
    $max    = intval ($max);

    $max    = $max < $min? $min: $max;
    $ret    = round (($max - $min) / 2) + $min;

    //fwrite (STDOUT, PHP_EOL . PHP_EOL . $ret . PHP_EOL . PHP_EOL);
    return ($ret);
}

/**
 * Override the password character pool for the test 
 */
class PasswordSubclass extends Password
{
    const CHARLIST  = 'AAAAAAAAAA';
}

/**
 * Test class for Password.
 * Generated by PHPUnit on 2011-12-17 at 18:10:33.
 */
class PasswordTest extends \PHPUnit_Framework_TestCase
{

    /**
     * @var gordian\reefknot\util\Password
     */
    protected $object;

    const PWMIN = 15;
    const PWMAX = 20;

    /**
     * Sets up the fixture, for example, opens a network connection.
     * This method is called before a test is executed.
     */
    protected function setUp ()
    {
    }

    /**
     * Tears down the fixture, for example, closes a network connection.
     * This method is called after a test is executed.
     */
    protected function tearDown ()
    {

    }

    public function testGetPassword ()
    {
        $this -> object = new PasswordSubclass (self::PWMIN, self::PWMAX);
        $pw = $this -> object -> getPassword ();
        $this -> assertTrue ((bool) preg_match ('/^A{' . self::PWMIN . ',' . self::PWMAX . '}$/', $pw));
        $this -> assertTrue (strlen ($pw) >= self::PWMIN);
        $this -> assertTrue (strlen ($pw) <= self::PWMAX);
        $this -> assertTrue ($pw === $this -> object -> getPassword ());
    }

    public function testGetPasswordFixedLen ()
    {
        $this -> object = new PasswordSubclass (self::PWMIN, self::PWMIN);
        $pw = $this -> object -> getPassword ();
        $this -> assertTrue ($pw === 'AAAAAAAAAAAAAAA');
        $this -> assertTrue ($pw === $this -> object -> getPassword ());
    }

    public function testGetPasswordFixedLen2 ()
    {
        $this -> object = new PasswordSubclass (self::PWMAX, self::PWMAX);
        $pw = $this -> object -> getPassword ();
        $this -> assertTrue ($pw === 'AAAAAAAAAAAAAAAAAAAA');
        $this -> assertTrue ($pw === $this -> object -> getPassword ());
    }

    public function testInvalidLenThrowsException ()
    {
        $exception  = NULL;
        try
        {
            $this -> object = new PasswordSubclass (self::PWMAX, self::PWMIN);
        }
        catch (\Exception $e)
        {
            $exception  = $e;
        }
        $this -> assertTrue ($exception instanceof \InvalidArgumentException);
    }
}

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.

GordonM
quelle
0

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.

Torbjørn
quelle
Tatsächlich ist die Klasse so konzipiert, dass das Kennwort beim ersten Aufruf von getPassword () generiert und dann zwischengespeichert wird, sodass für die Lebensdauer des Objekts immer dasselbe Kennwort zurückgegeben wird. Meine Testsuite überprüft bereits, dass mehrere Aufrufe von getPassword () auf derselben Kennwortinstanz immer dieselbe Kennwortzeichenfolge zurückgeben. Was die Thread-Sicherheit
angeht