Quali sono i buoni test unitari per coprire il caso d'uso del rotolamento di un dado?

18

Sto cercando di fare i conti con i test delle unità.

Diciamo che abbiamo un dado che può avere un numero predefinito di lati uguale a 6 (ma può essere 4, 5 lati ecc.):

import random
class Die():
    def __init__(self, sides=6):
        self._sides = sides

    def roll(self):
        return random.randint(1, self._sides)

I seguenti test sono validi / utili?

  • prova un tiro nel range 1-6 per un dado a 6 facce
  • prova un tiro di 0 per un dado a 6 facce
  • prova un lancio di 7 per un dado a 6 facce
  • prova un tiro nel raggio 1-3 per un dado a 3 lati
  • prova un tiro di 0 per un dado a 3 lati
  • prova un risultato di 4 per un dado a 3 lati

Sto solo pensando che si tratta di una perdita di tempo in quanto il modulo random è stato in giro abbastanza a lungo ma poi penso che se il modulo casuale viene aggiornato (ad esempio aggiorno la mia versione Python), allora almeno sono coperto .

Inoltre, ho anche bisogno di testare altre varianti di rulli di dado, ad es. il 3 in questo caso, o è buono coprire un altro stato di dado inizializzato?

    
posta Cybran 26.01.2014 - 10:53
fonte

8 risposte

22

Hai ragione, i tuoi test non dovrebbero verificare che il modulo random stia facendo il suo lavoro; un unittest dovrebbe testare solo la classe stessa, non come interagisce con altri codici (che dovrebbero essere testati separatamente).

Ovviamente è del tutto possibile che il tuo codice utilizzi random.randint() errato; o invece stai chiamando random.randrange(1, self._sides) e il tuo dado non getta mai il valore più alto, ma sarebbe un tipo diverso di bug, non uno che potresti catturare con un unittest. In questo caso, il tuo die unità funziona come progettato, ma il design stesso era difettoso.

In questo caso, userei il mocking per sostituire la funzione randint() , e verificare solo che sia stato chiamato correttamente. Python 3.3 e versioni successive sono dotati del modulo unittest.mock per gestire questo tipo di test, ma è possibile installare il pacchetto mock esterno su versioni precedenti per ottenere la stessa identica funzionalità

import unittest
try:
    from unittest.mock import patch
except ImportError:
    # < python 3.3
    from mock import patch


@patch('random.randint', return_value=3)
class TestDice(unittest.TestCase):
    def _make_one(self, *args, **kw):
        from die import Die
        return Die(*args, **kw)

    def test_standard_size(self, mocked_randint):
        die = self._make_one()
        result = die.roll()

        mocked_randint.assert_called_with(1, 6)
        self.assertEqual(result, 3)

    def test_custom_size(self, mocked_randint):
        die = self._make_one(sides=42)
        result = die.roll()

        mocked_randint.assert_called_with(1, 42)
        self.assertEqual(result, 3)


if __name__ == '__main__':
    unittest.main()

Con il mocking, il tuo test è ora molto semplice; ci sono solo 2 casi, davvero. Il caso predefinito per un dado a 6 facce e il caso per i lati personalizzati.

Esistono altri modi per sostituire temporaneamente la funzione randint() nello spazio dei nomi globale di Die , ma il modulo mock lo rende più semplice. Il @mock.patch decoratore qui si applica a tutti metodi di prova nel caso di test; ogni metodo di test viene passato un argomento extra, la funzione random.randint() derisa, quindi possiamo testare la simulazione per vedere se è stata effettivamente chiamata correttamente. L'argomento return_value specifica cosa viene restituito dal mock quando viene chiamato, quindi possiamo verificare che il metodo die.roll() abbia effettivamente restituito il risultato 'casuale' a noi.

Ho usato un'altra best practice inoppugnante di Python qui: importa la classe sotto test come parte del test. Il metodo _make_one esegue l'importazione e l'istanza di lavoro all'interno di un test , in modo che il modulo di test continui a essere caricato anche se hai fatto un errore di sintassi o un altro errore che impedire l'importazione del modulo originale.

In questo modo, se hai commesso un errore nel codice del modulo stesso, i test verranno comunque eseguiti; falliranno, ti spiegheranno l'errore nel tuo codice.

Per essere chiari, i test di cui sopra sono semplicistici all'estremo. L'obiettivo qui non è di testare che random.randint() sia stato chiamato con gli argomenti giusti, per esempio. Invece, l'obiettivo è quello di verificare che l'unità produca i risultati corretti in base a determinati input, in cui tali input includono i risultati delle altre unità non sotto test. Sfruttando il metodo random.randint() puoi prendere il controllo su un altro input del tuo codice.

Nei test nel mondo reale , il codice effettivo nella tua unità sotto test sarà più complesso; la relazione con gli input passati all'API e il modo in cui le altre unità vengono invocate possono essere comunque interessanti, e il mocking ti darà accesso ai risultati intermedi e ti consentirà di impostare i valori di ritorno per tali chiamate.

Ad esempio, nel codice che autentica gli utenti rispetto a un servizio OAuth2 di terzi (un'interazione a più stadi), vuoi verificare che il tuo codice stia trasmettendo i dati giusti a quel servizio di terze parti e ti consente di simulare diversi errori risposte che il servizio di terze parti restituirebbe, consentendo di simulare diversi scenari senza dover creare autonomamente un server OAuth2 completo. Qui è importante verificare che le informazioni provenienti da una prima risposta siano state gestite correttamente e siano state inoltrate a una seconda fase di chiamata, quindi vuoi vedere che il servizio deriso viene chiamato correttamente.

    
risposta data 26.01.2014 - 10:55
fonte
16

La risposta di Martijn è come lo faresti se volessi davvero eseguire un test che dimostri che tu stai chiamando random.randint. Tuttavia, a rischio di sentirsi dire "questo non risponde alla domanda", ritengo che questo non debba essere testato unitamente. Mocking randint non è più un test della scatola nera - stai specificando in particolare che alcune cose stanno accadendo nell'implementazione . Il test della scatola nera non è nemmeno un'opzione - non ci sono test da eseguire che dimostreranno che il risultato mai sarà inferiore a 1 o più di 6.

Puoi prendere in giro randint ? Si, puoi. Ma cosa stai dimostrando? Che lo hai chiamato con argomenti 1 e lati. Cosa significa che significa? Sei tornato al punto di partenza - alla fine della giornata finirai per dover provare - formalmente o informalmente - che chiamare random.randint(1, sides) implementa correttamente un lancio di dadi.

Sono tutto per il collaudo di unità. Sono fantastici controlli di sanità mentale e denunciano la presenza di bug. Tuttavia, non possono mai dimostrare la loro assenza, e ci sono cose che non possono essere asserite attraverso test (per esempio che una particolare funzione non genera mai un'eccezione o termina sempre.) In questo caso particolare, ritengo che ci sia ben poco da sopportare guadagno. Per un comportamento deterministico, i test unitari hanno senso perché sai in realtà quale sarà la risposta che ti aspetti.

    
risposta data 05.03.2014 - 21:16
fonte
6

Risolvi i semi casuali. Per dadi da 1, 2, 5 e 12 lati, conferma che alcune migliaia di tiri danno risultati compresi 1 e N, e non includono 0 o N + 1. Se per caso ti sembra di avere una serie di risultati casuali che non coprire l'intervallo previsto, passare a un seme diverso.

Gli strumenti di simulazione sono fantastici, ma solo perché ti permettono di fare una cosa non significa che la cosa dovrebbe essere fatta. YAGNI si applica ai dispositivi di prova tanto quanto le caratteristiche.

Se puoi facilmente testare con dipendenze non bloccate, dovresti farlo quasi sempre; in questo modo i test saranno focalizzati sulla riduzione dei conteggi dei difetti, non solo aumentando il conteggio dei test. L'eccesso di derisione rischia di creare figure di copertura fuorvianti, che a loro volta possono portare a rinviare i test effettivi in una fase successiva, forse non hai mai il tempo di andare a ...

    
risposta data 06.03.2014 - 00:17
fonte
2

Seminare il generatore di numeri casuali e verificare i risultati attesi NON è, per quanto posso vedere, un test valido. Fa supposizioni su COME i tuoi dadi funzionano internamente, che è cattivo-cattivo. Gli sviluppatori di python potrebbero cambiare il generatore di numeri casuali, o il dado (NOTA: "i dadi" sono plurali, "morire" è singolare. A meno che la tua classe implementi più tiri di dado in una chiamata, dovrebbe probabilmente chiamarsi "morire") potrebbe usa un generatore di numeri casuali diverso.

Allo stesso modo, il mocking della funzione casuale presuppone che l'implementazione della classe funzioni esattamente come previsto. Perché questo potrebbe non essere il caso? Qualcuno potrebbe prendere il controllo del generatore di numeri casuali predefinito di python e, per evitare ciò, una versione futura del tuo dado può recuperare diversi numeri casuali, o numeri casuali più grandi, per mescolare in più dati casuali. Uno schema simile è stato utilizzato dai produttori del sistema operativo FreeBSD, quando sospettavano che l'NSA stesse manomettendo i generatori di numeri casuali hardware incorporati nelle CPU.

Se fossi in me, eseguirò, ad esempio, 6000 rotoli, li contiamo e assicurarmi che ogni numero compreso tra 1 e 6 venga fatto rotolare tra 500 e 1500 volte. Vorrei anche verificare che non vengano restituiti numeri al di fuori di tale intervallo. Potrei anche verificare che, per un secondo set di 6000 rotoli, quando ordinate [1..6] in ordine di frequenza, il risultato è diverso (questo fallirà una volta su 720 corse, se i numeri sono casuali!). Se vuoi essere accurato, potresti trovare la frequenza dei numeri dopo un 1, dopo un 2, ecc; ma assicurati che la dimensione del tuo campione sia abbastanza grande e che tu abbia abbastanza varianza. Gli umani si aspettano che i numeri casuali abbiano meno schemi di quelli che effettivamente fanno.

Ripeti per un dado a 12 facciate e a 2 facciate (6 è il più usato, quindi è il più atteso per chiunque scriva questo codice).

Infine, testerei per vedere cosa succede con un dado a 1 faccia, un dado a 0 facce, un dado a 1 lato, un dado a 2,3 facce, un [1,2,3,4,5,6] lati muori e un dado "blah". Naturalmente, tutti dovrebbero fallire; falliscono in un modo utile? Questi dovrebbero probabilmente fallire nella creazione, non nel rotolamento.

O forse vuoi anche gestirli diversamente - forse creare un dado con [1,2,3,4,5,6] dovrebbe essere accettabile - e forse anche "blah"; questo potrebbe essere un dado con 4 facce e ogni faccia ha una lettera sopra. Mi viene in mente il gioco "Boggle", così come una palla magica.

E infine, potresti volerlo prendere in considerazione: link

    
risposta data 06.03.2014 - 19:53
fonte
2

Che cos'è un Die se ci pensi? - non più di un wrapper attorno a random . Incapsula random.randint e la riporta in termini del vocabolario della tua applicazione: Die.Roll .

Non trovo rilevante inserire un altro livello di astrazione tra Die e random perché Die stesso è già questo livello di riferimento indiretto tra l'applicazione e la piattaforma.

Se vuoi ottenere risultati di dadi in scatola, solo finta Die , non simulare random .

In generale, non collaudo unitamente i miei oggetti wrapper che comunicano con sistemi esterni, scrivo per loro test di integrazione. Potresti scrivere un paio di quelli per Die ma, come hai sottolineato, a causa della natura casuale dell'oggetto sottostante, non saranno significativi. Inoltre, non c'è nessuna configurazione o comunicazione di rete coinvolta, quindi non c'è molto da testare tranne una chiamata alla piattaforma.

= > Considerando che Die è solo poche semplici righe di codice e aggiunge poca o nessuna logica rispetto a random stesso, vorrei saltarlo testandolo in quello specifico esempio.

    
risposta data 24.03.2014 - 15:37
fonte
2

A rischio di nuotare controcorrente, ho risolto questo problema esatto alcuni anni fa usando un metodo non ancora menzionato.

La mia strategia consisteva semplicemente nel prendere in giro l'RNG con uno che producesse un flusso prevedibile di valori che coprivano l'intero spazio. Se (per esempio) side = 6 e l'RNG produce valori da 0 a 5 in sequenza, posso prevedere come dovrebbe comportarsi la mia classe e il test dell'unità di conseguenza.

La logica è che questo test la logica in questa classe da sola, partendo dal presupposto che l'RNG alla fine produrrà ciascuno di questi valori e senza testare l'RNG stesso.

È semplice, deterministico, riproducibile e cattura gli insetti. Vorrei usare ancora la stessa strategia.

La domanda non specifica quali dovrebbero essere i test, solo quali dati potrebbero essere utilizzati per il test, data la presenza di un RNG. Il mio suggerimento è semplicemente quello di testare in modo esauriente prendendo in giro il RNG. La domanda su cosa vale la pena di test dipende dalle informazioni non fornite nella domanda.

    
risposta data 24.03.2014 - 15:06
fonte
1

I test suggeriti nella tua domanda non rilevano un contatore aritmetico modulare come implementazione. Inoltre, non rilevano errori di implementazione comuni nel codice correlato alla distribuzione di probabilità come return 1 + (random.randint(1,maxint) % sides) . O una modifica al generatore che si traduce in modelli bidimensionali.

Se in realtà vuoi verificare che stai generando numeri apparentemente distribuiti in modo uniforme, devi controllare una vasta gamma di proprietà. Per fare un buon lavoro, potresti eseguire link sui tuoi numeri generati. Oppure scrivi una suite di test unitaria altrettanto complessa.

Non è colpa dei test unitari o TDD, la casualità è una proprietà molto difficile da verificare. E un argomento popolare per gli esempi.

    
risposta data 06.03.2014 - 18:53
fonte
-1

Il test più semplice di un tiro di dado è semplicemente ripeterlo diverse centinaia di migliaia di volte, e convalidare che ogni risultato possibile è stato colpito approssimativamente (1 / numero di lati) volte. Nel caso di un dado a 6 facce, dovresti vedere ogni possibile valore colpito circa il 16,6% delle volte. Se uno è fuori più di un percento, allora hai un problema.

Facendolo in questo modo evita ti permette di rifattare la meccanica sottostante di generare un numero casuale facilmente e, soprattutto, senza cambiare il test.

    
risposta data 24.03.2014 - 20:37
fonte

Leggi altre domande sui tag