Metodi di test unitario con uscita indeterminata

36

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

Sto costruendo test unitari e mi sono imbattuto in un piccolo intoppo interessante con questa classe. L'idea alla base di un test unitario è che dovrebbe essere ripetibile. Se corri il test un centinaio di volte, dovrebbe dare gli stessi risultati un centinaio di volte. Se stai dipendendo da una risorsa che può o non può esserci o potrebbe o meno essere nello stato iniziale che ti aspetti, allora sei obbligato a prendere in giro la risorsa in questione per assicurarti che il tuo test sia sempre ripetibile.

Ma 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 specifichi un intervallo di lunghezze accettabili (diciamo 15 - 20 caratteri), ora hai il problema che potresti eseguire il test un centinaio di volte e ottenere 100 passaggi ma sulla 101esima corsa potresti ottenere una stringa di 9 caratteri indietro.

Nel caso della classe password, che è abbastanza semplice nel suo nucleo, non dovrebbe dimostrare un grosso problema. Ma mi ha fatto pensare al caso generale. Qual è la strategia che viene generalmente accettata come la migliore da prendere quando si ha a che fare con SUT che generano output indeterminati per progetto?

    
posta GordonM 01.01.2012 - 20:03
fonte

11 risposte

20

L'output "non deterministico" dovrebbe avere un modo per diventare deterministico ai fini dei test unitari. 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);

Potresti creare una versione di prova specializzata della funzione che restituisce qualsiasi sequenza di numeri che vuoi assicurarti che il test sia completamente ripetibile. Nel programma reale, puoi avere un'implementazione predefinita che potrebbe essere il fallback se non sovrascritta.

    
risposta data 01.01.2012 - 20:25
fonte
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 un determinato risultato ogni volta seminando il tuo generatore di password con lo stesso valore ogni volta.

    
risposta data 01.01.2012 - 20:24
fonte
14

Test contro "il contratto". Quando i metodi sono definiti come "genera password lunghe da 15 a 20 caratteri con a-z", testalo in questo modo

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

In aggiunta puoi estrarre la generazione, quindi tutto ciò che si basa su di esso può essere testato usando un'altra classe di generatore "statica"

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; }
}
    
risposta data 01.01.2012 - 20:23
fonte
6

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

Come hai affermato nella domanda, random rende l'output non deterministico poiché è stato globale . Significa che accede a qualcosa al di fuori del sistema per generare valori.

Non puoi mai eliminare 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 prendere in giro il RandomSource per i tuoi test.

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

    
risposta data 01.01.2012 - 22:06
fonte
3

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

{*} Tale test viola il principio "fai il test veloce" per il test dell'unità, quindi potresti sentirti meglio caratterizzandoli in qualche altro modo: test di accettazione o test di regressione, per esempio. Comunque, ho usato il mio framework di test delle unità.

    
risposta data 01.01.2012 - 22:24
fonte
3

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

  1. overfitting
  2. Impraticabilità

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

Quindi cosa intendo con questo? Beh, per overfitting intendo un tipico problema di test statistici: l'overfitting si verifica quando si prova un algoritmo stocastico contro un set troppo vincolato di dati. Se poi si torna indietro e si raffina il proprio algoritmo, implicitamente si adattano molto bene i dati di allenamento (per errore misura il proprio algoritmo ai dati del test), ma tutti gli altri dati potrebbero non essere affatto ( perché non hai mai provato contro di esso).

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

Se rendi deterministici i tuoi test facendo in modo che il generatore di numeri casuali si innesti, devi sempre testare lo stesso insieme di dati molto piccolo e (di solito) non rappresentativo . Questo distorce i tuoi dati e può portare a pregiudizi nella tua funzione.

Il secondo punto, impraticabilità, sorge quando non si ha alcun controllo sulla variabile stocastica. Questo di solito non accade con i generatori di numeri casuali (a meno che non si abbia bisogno di una "vera" fonte di casualità), ma può succedere quando stocastici si intrufolano nel tuo problema in altri modi. Ad esempio, quando si verifica un codice concorrente: le condizioni di gara sono sempre stocastiche, non è possibile (facilmente) renderle deterministiche.

L'unico modo per aumentare la sicurezza in questi casi è test molto . Mescolare, sciacquare, ripetere. Ciò aumenta la fiducia, fino a un certo livello (a quel punto il trade-off per ulteriori test run diventa trascurabile).

    
risposta data 02.01.2012 - 22:33
fonte
2

In realtà hai più responsabilità qui. Il test unitario e in particolare TDD è ottimo per evidenziare questo tipo di cose.

Le responsabilità sono:

1) Generatore di numeri casuali. 2) Password formattatore.

Il formatter password utilizza il generatore di numeri casuali. Inietta il generatore nel tuo formattatore 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 simulato.

Non solo ottieni un codice migliore per ottenere test migliori.

    
risposta data 02.01.2012 - 02:31
fonte
2

Come gli altri hanno già menzionato, unit test questo codice rimuovendo la casualità.

Potresti anche volere un test di livello superiore che lasci il generatore di numeri casuali in posizione, testa solo il contratto (lunghezza della password, caratteri consentiti, ...) e, in caso di fallimento, scarica abbastanza informazioni per permetterti di riproduce lo stato del sistema nell'istanza in cui il test casuale ha avuto esito negativo.

Non importa se il test in sé non è ripetibile, a patto che tu possa trovare il motivo per cui non è riuscito a farlo questa volta.

    
risposta data 02.01.2012 - 10:12
fonte
2

Molte difficoltà di test delle unità diventano banali quando si effettua il refactoring del codice per separare 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 ciò che intendo fare?". Nel tuo caso, non sai cosa intendi fare il codice perché non è deterministico.

Con questa mente, separa la tua logica in parti piccole, facilmente comprensibili, facilmente testate e isolate. Nello specifico, crei un metodo distinto (o classe!) Che prende come sorgente una fonte di casualità e produce la password come output. Quel codice è chiaramente deterministico.

Nel tuo test dell'unità, lo alimenti sempre con lo stesso input non proprio casuale. Per flussi casuali molto piccoli, basta codificare i valori nel test. Altrimenti, fornire un seme costante all'RNG nel test.

A un livello più alto di test (chiamalo "accettazione" o "integrazione" o qualsiasi altra cosa), lascerai che il codice venga eseguito con una vera fonte casuale.

    
risposta data 02.01.2012 - 22:31
fonte
1

La maggior parte delle risposte precedenti indica che il mocking del generatore di numeri casuali è la strada da percorrere, tuttavia stavo semplicemente usando la funzione integrata mt_rand. Consentire il mocking avrebbe significato riscrivere la classe per richiedere che un generatore di numeri casuali venisse iniettato in fase di costruzione.

O così ho pensato!

Una delle conseguenze dell'aggiunta di spazi dei nomi è che il mocking delle 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 dell'unità in quello spazio dei nomi, e sarà usato 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 parlarne, perché l'override delle 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 con questo.

    
risposta data 02.01.2012 - 09:46
fonte
0

Esiste un test aggiuntivo da includere in questa situazione, e questo è uno per garantire che le chiamate ripetute al generatore di password producano effettivamente password diverse. Se hai bisogno di un generatore di password sicuro per i thread, dovresti anche provare le chiamate simultanee usando più thread.

Questo in pratica garantisce che stai usando la tua funzione casuale correttamente, e non ri-semina su ogni chiamata.

    
risposta data 02.01.2012 - 10:05
fonte

Leggi altre domande sui tag