Metodi di test unitari con output indeterminato


37

Ho una classe che ha lo scopo di generare una password casuale di una lunghezza che è anche casuale, ma limitata a essere compresa tra una lunghezza minima e massima definita.

Sto costruendo unit test e ho incontrato un piccolo inconveniente interessante con questa classe. L'idea alla base di un test unitario è che dovrebbe essere ripetibile. Se si esegue il test cento volte, si dovrebbero ottenere gli stessi risultati cento volte. Se dipendi da una risorsa che può essere o non essere presente o potrebbe essere o non essere nello stato iniziale che ti aspetti, allora devi prendere in giro la risorsa in questione per assicurarti che il test sia sempre ripetibile.

Ma che dire nei casi in cui si suppone che il SUT generi un output indeterminato?

Se aggiusto la lunghezza minima e massima allo stesso valore, posso facilmente verificare che la password generata sia della lunghezza prevista. Ma se specifico un intervallo di lunghezze accettabili (diciamo 15-20 caratteri), ora hai il problema di poter eseguire il test centinaia di volte e ottenere 100 passaggi, ma al 101 ° giro potresti recuperare una stringa di 9 caratteri.

Nel caso della classe password, che è abbastanza semplice nel suo nucleo, non dovrebbe rivelarsi un grosso problema. Ma mi ha fatto pensare al caso generale. Qual è la strategia che di solito viene accettata come la migliore da prendere quando si tratta di SUT che generano output indeterminati in base alla progettazione?


9
Perché i voti stretti? Penso che sia una domanda perfettamente valida.
Mark Baker

Eh, grazie per il commento. Non l'ho nemmeno notato, ma ora mi chiedo la stessa cosa. L'unica cosa che mi viene in mente è che si tratta di un caso generale piuttosto che di uno specifico, ma potrei semplicemente pubblicare la fonte per la classe di password sopra menzionata e chiedere "Come posso testare quella classe?" invece di "Come testare una classe indeterminata?"
GordonM,

1
@MarkBaker Perché la maggior parte delle domande poco impegnative si trovano su programmers.se. È un voto per la migrazione, non per chiudere la domanda.
Ikke,

Risposte:


20

L'output "non deterministico" dovrebbe avere un modo di diventare deterministico ai fini del test unitario. Un modo per gestire la casualità è consentire la sostituzione del motore casuale. Ecco un esempio (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);

È possibile creare una versione di prova specializzata della funzione che restituisce qualsiasi sequenza di numeri per assicurarsi che il test sia completamente ripetibile. Nel programma reale, è possibile avere un'implementazione predefinita che potrebbe essere il fallback se non sovrascritto.


1
Tutte le risposte fornite avevano buoni suggerimenti che ho usato, ma questa è quella che penso risolva il problema principale in modo che ottenga l'accettazione.
GordonM,

1
Praticamente lo inchioda sulla testa. Sebbene non deterministico, ci sono ancora dei limiti.
surfasb,

21

La password di output effettiva potrebbe non essere determinata ogni volta che viene eseguito il metodo, ma avrà comunque determinate funzionalità che possono essere testate, come lunghezza minima, caratteri che rientrano in un determinato set di caratteri, ecc.

Puoi anche verificare che la routine restituisca ogni volta un risultato determinato eseguendo il seeding del generatore di password con lo stesso valore ogni volta.


La classe PW mantiene una costante che è essenzialmente il pool di caratteri da cui dovrebbe essere generata la password. Sottoclassandolo e sovrascrivendo la costante con un singolo carattere sono riuscito a eliminare un'area di non determinanza ai fini del test. Quindi grazie.
GordonM,

14

Test contro "il contratto". Quando il metodo è definito come "genera password di lunghezza compresa tra 15 e 20 caratteri con az", testalo in questo modo

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

Inoltre è possibile estrarre la generazione, quindi tutto, che si basa su di essa, può essere testato utilizzando un'altra classe di generatori "statici"

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; }
}

Il regex che hai dato si è rivelato utile, quindi ho incluso una versione ottimizzata nel mio test. Grazie.
GordonM,

6

Hai un Password generator e hai bisogno di una fonte casuale.

Come hai affermato nella domanda, un randomoutput non deterministico è lo stato globale . Significa che accede a qualcosa al di fuori del sistema per generare valori.

Non puoi mai sbarazzarti di qualcosa del genere per tutte le tue classi, ma puoi separare la generazione della password per la creazione di valori casuali.

<?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));
        }
    }

}

Se strutturi il codice in questo modo puoi deridere il RandomSource per i tuoi test.

Non sarai in grado di testare al 100% il RandomSourcema i suggerimenti che hai ricevuto per testare i valori in questa domanda possono essere applicati ad esso (come il test che rand->(1,26);restituisce sempre un numero da 1 a 26.


Questa è un'ottima risposta.
Nick Hodges,

3

Nel caso di una fisica delle particelle Monte Carlo, ho scritto "unit test" {*} che invocano la routine non deterministica con un seme casuale predefinito , quindi eseguo un numero statistico di volte e verificano la presenza di violazioni dei vincoli (livelli di energia sopra l'energia in ingresso deve essere inaccessibile, tutti i passaggi devono selezionare un certo livello, ecc.) e le regressioni rispetto ai risultati precedentemente registrati.


{*} Tale test viola il principio "velocizza il test" per i test unitari, quindi potresti sentirti meglio caratterizzandoli in qualche altro modo: test di accettazione o test di regressione, per esempio. Tuttavia, ho usato il mio framework di test unitari.


3

Non sono d'accordo con la risposta accettata , per due motivi:

  1. sovradattamento
  2. irrealizzabilità

(Nota che potrebbe essere una buona risposta in molte circostanze, ma non in tutti, e forse non nella maggior parte.)

Quindi cosa intendo con questo? Bene, per overfitting intendo un tipico problema del testing statistico: il sovrafitting si verifica quando si verifica un algoritmo stocastico su un insieme di dati eccessivamente vincolato. Se poi torni indietro e perfezioni il tuo algoritmo, implicitamente lo adatterai molto bene ai dati di allenamento ( adatterai accidentalmente il tuo algoritmo ai dati di test), ma forse tutti gli altri dati potrebbero non essere affatto (perché non esegui mai dei test) .

(Per inciso, questo è sempre un problema che si nasconde con i test unitari. Ecco perché i buoni test sono completi , o almeno rappresentativi per una data unità, e questo è difficile in generale.)

Se rendi deterministici i tuoi test rendendo collegabile il generatore di numeri casuali, esegui sempre il test con lo stesso set di dati molto piccolo e (di solito) non rappresentativo . Ciò altera i dati e può causare distorsioni nella funzione.

Il secondo punto, impraticabilità, sorge quando non si ha alcun controllo sulla variabile stocastica. Questo di solito non succede con i generatori di numeri casuali (a meno che tu non abbia bisogno di una "vera" fonte di random), ma può accadere quando gli stocastici si insinuano nel tuo problema in altri modi. Ad esempio, quando si testano codici simultanei: le condizioni di gara sono sempre stocastiche, non è possibile (facilmente) renderle deterministiche.

L'unico modo per aumentare la fiducia in questi casi è testare molto . Raccogliere, sciacquare, ripetere. Ciò aumenta la fiducia, fino a un certo livello (a quel punto il compromesso per ulteriori test diventa trascurabile).


2

In realtà hai più responsabilità qui. Test unitari e in particolare TDD sono ottimi per evidenziare questo genere di cose.

Le responsabilità sono:

1) Generatore di numeri casuali. 2) Formatter password.

Il formattatore della password utilizza il generatore di numeri casuali. Iniettare il generatore nel formatter tramite il suo costruttore come interfaccia. Ora puoi testare completamente il tuo generatore di numeri casuali (test statistico) e puoi testare il formattatore iniettando un generatore di numeri casuali beffardo.

Non solo ottieni codice migliore, ma anche test migliori.


2

Come già menzionato dagli altri, il test unitario questo codice rimuovendo la casualità.

Potresti anche voler avere un test di livello superiore che lasci il generatore di numeri casuali sul posto, verifichi solo il contratto (lunghezza della password, caratteri consentiti, ...) e, in caso di errore, scarica informazioni sufficienti per consentirti di riprodurre il sistema stato nell'unica istanza in cui il test casuale non è riuscito.

Non importa che il test stesso non sia ripetibile, purché si riesca a trovare il motivo per cui questa volta non è riuscito.


2

Molte difficoltà di test unitari diventano banali quando si refactoring il codice per recidere le dipendenze. Un database, un file system, l'utente o, nel tuo caso, una fonte di casualità.

Un altro modo di guardare è che i test unitari dovrebbero rispondere alla domanda "questo codice fa quello che intendo fare?". Nel tuo caso, non sai cosa intendi fare dal codice perché non è deterministico.

Con questa mente, separa la tua logica in parti piccole, facilmente comprensibili e facilmente testate in isolamento. In particolare, si crea un metodo distinto (o classe!) Che accetta una fonte di casualità come input e produce la password come output. Quel codice è chiaramente deterministico.

Nel test unitario, lo si alimenta sempre con lo stesso input non del tutto casuale. Per flussi casuali molto piccoli, codifica semplicemente i valori nel tuo test. Altrimenti, fornire un seme costante all'RNG nel test.

A un livello superiore di test (chiamalo "accettazione" o "integrazione" o altro), lascerai che il codice venga eseguito con una vera fonte casuale.


Questa risposta l'ha inchiodata per me: avevo davvero due funzioni in una: il generatore di numeri casuali e la funzione che faceva qualcosa con quel numero casuale. Ho semplicemente eseguito il refactoring e ora posso facilmente testare la parte non deterministica del codice e fornirgli i parametri generati dalla parte casuale. La cosa bella è che posso quindi alimentarlo (diversi set di) parametri fissi nel mio test unitario (sto usando un generatore di numeri casuali dalla libreria standard, quindi non testare comunque questo).
neuronet,

1

La maggior parte delle risposte precedenti indica che prendere in giro il generatore di numeri casuali è la strada da percorrere, tuttavia stavo semplicemente usando la funzione mt_rand integrata. Consentire il derisione avrebbe significato riscrivere la classe per richiedere l'iniezione di un generatore di numeri casuali al momento della costruzione.

O almeno così pensavo!

Una delle conseguenze dell'aggiunta di spazi dei nomi è che il deridere incorporato nelle funzioni PHP è passato da incredibilmente difficile a banalmente semplice. Se il SUT si trova in un determinato spazio dei nomi, tutto ciò che devi fare è definire la tua funzione mt_rand nel test unitario in quello spazio dei nomi e verrà utilizzata al posto della funzione PHP integrata per la durata del test.

Ecco la suite di test finalizzata:

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);
    }
}

Ho pensato di menzionarlo, perché sovrascrivere le funzioni interne di PHP è un altro uso per gli spazi dei nomi che semplicemente non mi era venuto in mente. Grazie a tutti per l'aiuto in questo.


0

C'è un test aggiuntivo che dovresti includere in questa situazione, e quello è quello di garantire che le chiamate ripetute al generatore di password producano effettivamente password diverse. Se hai bisogno di un generatore di password thread-safe, dovresti anche testare le chiamate simultanee usando più thread.

Questo in pratica assicura che stai usando correttamente la tua funzione casuale e non esegui di nuovo il seeding ad ogni chiamata.


In realtà, la classe è progettata in modo tale che la password venga generata alla prima chiamata a getPassword () e poi si blocchi, quindi restituisce sempre la stessa password per la durata dell'oggetto. La mia suite di test verifica già che più chiamate a getPassword () sulla stessa istanza di password restituiscano sempre la stessa stringa di password. Per quanto riguarda la sicurezza dei thread, questo non è un problema in PHP :)
GordonM,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.