Test unitario con funzioni che restituiscono risultati casuali

Non penso che questo sia specifico per una lingua o un framework, ma sto usando xUnit.net e C #.

Ho una funzione che restituisce una data casuale in un determinato intervallo. Passo una data e la data di ritorno è sempre compresa nell’intervallo da 1 a 40 anni prima della data indicata.

Ora mi chiedo solo se c’è un buon modo per testare questo. L’approccio migliore sembra essere quello di creare un ciclo e lasciare che la funzione funzioni 100 volte e affermare che ognuno di questi 100 risultati si trova nell’intervallo desiderato, che è il mio approccio attuale.

Mi rendo anche conto che, a meno che non sia in grado di controllare il mio generatore Random, non ci sarà una soluzione perfetta (dopotutto, il risultato è casuale), ma mi chiedo quale approccio si prende quando si deve testare la funzionalità che restituisce un risultato casuale in un certo intervallo?

Oltre a verificare che la funzione restituisca una data nell’intervallo desiderato, si desidera garantire che il risultato sia ben distribuito. Il test che descrivi passerebbe una funzione che ha semplicemente restituito la data di invio!

Quindi oltre a chiamare la funzione più volte e verificare che il risultato rimanga nell’intervallo desiderato, proverei anche a valutare la distribuzione, magari inserendo i risultati in bucket e controllando che i bucket abbiano un numero approssimativamente uguale di risultati dopo che sei fatto. Potrebbero essere necessarie più di 100 chiamate per ottenere risultati stabili, ma ciò non sembra una funzione costosa (runtime), quindi è ansible eseguirlo facilmente per alcune iterazioni K.

Ho avuto un problema prima con funzioni “casuali” non uniformi … possono essere un vero dolore, vale la pena di provarlo per tempo.

Mock o falso il generatore di numeri casuali

Fai qualcosa del genere … Non l’ho compilato, quindi potrebbero esserci alcuni errori di syntax.

public interface IRandomGenerator { double Generate(double max); } public class SomethingThatUsesRandom { private readonly IRandomGenerator _generator; private class DefaultRandom : IRandomGenerator { public double Generate(double max) { return (new Random()).Next(max); } } public SomethingThatUsesRandom(IRandomGenerator generator) { _generator = generator; } public SomethingThatUsesRandom() : this(new DefaultRandom()) {} public double MethodThatUsesRandom() { return _generator.Generate(40.0); } } 

Nel tuo test, basta fingere o deridere l’IRandomGenerator per restituire qualcosa in scatola.

Penso che ci siano tre diversi aspetti di questo problema da testare.

Il primo: il mio algoritmo è quello giusto? Cioè, dato un generatore di numeri casuali funzionante correttamente, produrrà date che sono distribuite casualmente su tutta la gamma?

Il secondo: l’algoritmo gestisce correttamente i casi limite? Cioè, quando il generatore di numeri casuali produce i valori massimi o minimi consentiti, qualcosa si rompe?

Il terzo: la mia implementazione dell’algoritmo funziona? Cioè, dato un elenco noto di input pseudo-casuali, sta producendo l’elenco atteso di date pseudo-casuali?

Le prime due cose non sono qualcosa che vorrei integrare nella suite di test delle unità. Sono qualcosa che proverei mentre progettavo il sistema. Probabilmente lo farei scrivendo un cablaggio di prova che ha generato una data di zillion ed eseguito un test del chi quadrato, come suggerito da daniel.rikowski. Mi assicuro inoltre che questa imbracatura di test non si sia conclusa finché non ha gestito entrambi i casi limite (supponendo che il mio intervallo di numeri casuali sia abbastanza piccolo da consentirne il superamento). E lo documenterei, in modo che chiunque si avvicini e cerchi di migliorare l’algoritmo sappia che questo è un cambiamento decisivo.

L’ultimo è qualcosa per cui farei un test unitario. Devo sapere che nulla è penetrato nel codice che interrompe la sua implementazione di questo algoritmo. Il primo segnale che otterrò quando ciò accadrà è che il test fallirà. Poi tornerò al codice e scoprirò che qualcun altro pensava che stavano aggiustando qualcosa e invece lo ruppe. Se qualcuno ha risolto l’algoritmo, sarebbe su di loro anche per risolvere questo test.

Non è necessario controllare il sistema per rendere deterministici i risultati. Hai un approccio corretto: decidi cosa è importante per l’output della funzione e prova per quello. In questo caso, è importante che il risultato sia compreso in un intervallo di 40 giorni e si sta verificando per quello. È anche importante che non restituisca sempre lo stesso risultato, quindi prova anche quello. Se vuoi essere più appassionato, puoi verificare che i risultati superino un test di casualità.

Normalmente utilizzo esattamente il tuo approccio suggerito: controlla il generatore casuale. Inizializzalo per test con un seed predefinito (o sostituiscilo con un proxy che restituisce numeri che si adattano ai miei test), quindi ho un comportamento deterministico / verificabile.

Se si desidera verificare la qualità dei numeri casuali (in termini di indipendenza) ci sono diversi modi per farlo. Un buon modo è il test del Chi quadrato .

A seconda di come la tua funzione crea la data casuale, potresti anche voler controllare date illegali: anni bisestili impossibili, o il 31esimo giorno di un mese di 30 giorni.

I metodi che non mostrano un comportamento deterministico non possono essere adeguatamente testati unitamente, poiché i risultati differiscono da un’esecuzione all’altra. Un modo per aggirare questo è quello di seminare il generatore di numeri casuali con un valore fisso per il test dell’unità. È anche ansible estrarre la casualità della class di generazione della data (e quindi applicare il Principio di Responsabilità Unica ) e iniettare valori noti per i test unitari.

Certo, l’uso di un generatore di numeri casuali con seme fisso funzionerà benissimo, ma anche in questo caso stai semplicemente provando a testare ciò che non puoi prevedere. Che va bene È equivalente ad avere un sacco di test fissi. Tuttavia, ricorda: prova ciò che è importante, ma non provare a testare tutto. Credo che i test a caso siano un modo per provare a testare tutto, e non è efficiente (o veloce). Potresti potenzialmente dover eseguire moltissimi test randomizzati prima di colpire un bug.

Quello che sto cercando di ottenere qui è che dovresti semplicemente scrivere un test per ogni bug che trovi nel tuo sistema. Esegui il test dei casi limite per assicurarti che la tua funzione funzioni anche nelle condizioni più estreme, ma in realtà è il meglio che puoi fare senza spendere troppo tempo o fare rallentare i test dell’unità, o semplicemente sprecare cicli del processore.

Suggerirei di sovrascrivere la funzione casuale. Sono un test unitario in PHP, quindi scrivo questo codice:

 // If we are unit testing, then... if (defined('UNIT_TESTING') && UNIT_TESTING) { // ...make our my_rand() function deterministic to aid testing. function my_rand($min, $max) { return $GLOBALS['random_table'][$min][$max]; } } else { // ...else make our my_rand() function truly random. function my_rand($min = 0, $max = PHP_INT_MAX) { if ($max === PHP_INT_MAX) { $max = getrandmax(); } return rand($min, $max); } } 

Quindi imposto il random_table come richiesto per test.

Testare la casualità reale di una funzione casuale è completamente un test separato. Eviterei di testare la casualità nei test unitari e invece farei test separati e google la vera casualità della funzione casuale nel linguaggio di programmazione che stai usando. I test non deterministici (se del caso) dovrebbero essere lasciati fuori dai test unitari. Forse hanno una suite separata per quei test, che richiede input umani o tempi di esecuzione molto più lunghi per ridurre al minimo le probabilità di un errore che è davvero un passaggio.

Non penso che il test unitario sia pensato per questo. È ansible utilizzare il test dell’unità per le funzioni che restituiscono un valore stocastico, ma utilizzare un seme fisso, nel qual caso in un modo che non sono stocastici, per così dire, per seme casuale, non penso che il test dell’unità sia ciò che si desidera, ad esempio per gli RNG ciò che intendi avere è un test di sistema, nel quale esegui molte volte l’RNG e ne guardi la distribuzione o i momentjs.