Quali sono i modi migliori per unità di codice di test che restituisce sequenze casuali che soddisfano condizioni specifiche, come le catene Markov?
Siamo specifici. Ci sono due cose naturali da testare:
-
Che lo stato iniziale restituito in ogni catena segue le probabilità specificate dall'utente, in modo che
chain[0] == i
con probabilitàp_initial[i]
. -
Che le transizioni tra stati siano corrette, ovvero se
chain[k] == i
, allora la probabilità dichain[k+1] == j
è data dap_transition[i, j]
.
Ora, posso pensare a due modi per farlo.
- Verifica che il codice faccia quello che voglio che faccia. Genera un gran numero di sequenze (ad esempio, 10000) e verifica che le frequenze per lo stato iniziale siano approssimative a
p_initial
. Prossimo test che le frequenze delle transizioni allo statoj
dallo statoi
seguanop_transition
.
La difficoltà con questo approccio è che qualsiasi campione finito mostrerà fluttuazioni dalle probabilità prescritte. Devo quindi impostare un livello accettabile per queste fluttuazioni. Ma questo significa che a) il test potrebbe perdere gli errori effettivi nel codice se cambiano solo le frequenze un po 'e b) dopo un refactoring potrei ottenere falsi fallimenti a causa di una particolare catena che cade al di fuori del livello di fluttuazione accettato per puro caso . (Suppongo che sto usando un seme fisso per il generatore di numeri casuali in modo che almeno senza refactoring i test debbano essere riproducibili.) Peggio, più mi rilassano i criteri di accettazione per mitigare il problema b), più rischio di cadere per a).
Inoltre, sto solo testando un piccolo sottoinsieme dei requisiti per il modello (voglio anche che le transizioni siano indipendenti, per esempio). E rispetto a quanto sia semplice l'implementazione di questo codice (dato un generatore di numeri casuali, vedi sotto), non sembra che ne valga la pena aggiungere un po 'di dettagli nel codice di test.
- L'alternativa è supporre che il generatore di numeri casuali sia stato testato dai suoi creatori e funzioni bene, e basta testare che il mio codice lo chiami nel modo giusto. Quindi posso prendere in giro l'RNG e controllare che il mio codice lo chiami con gli argomenti giusti.
Il problema con questo approccio è che rende il test strongmente dipendente dall'implementazione. Per esempio, potrei scegliere lo stato iniziale usando qualcosa come numpy.random.choice
in Python, oppure potrei semplicemente generare un numero casuale compreso tra 0 e 1 ( numpy.random.random
) e implementare la mia logica per scegliere lo stato iniziale basato su quello. Anche all'interno di ciascuna di queste scelte, potrei scegliere di ordinare gli stati in qualsiasi modo. I test devono sapere cosa sta facendo il codice e questo rende i test fragili per il refactoring.
Quindi questo è il mio problema: sembra sbagliato accoppiare così tanto il test all'implementazione. Ma sembra anche sbagliato eseguire molti test della mia funzione che sono in effetti test del RNG (e neanche test particolarmente severi).
C'è un modo diverso e migliore per farlo che mi permette di testare il codice che ho scritto facendo affidamento sui writer RNG per avere controllato il loro codice?
Per definizione, ecco un codice (Python) che implementa un semplice generatore di catene Markov
import numpy as np
class PlainMarkov(object):
def __init__(self, p_initial, p_transition, rng=None):
""" p_initial = sequence of initial-state probabilities
p_transition = sequence of sequences for transition probabilities
rng = random number generator
"""
self.p_initial = np.asarray(p_initial)
self.p_transition = np.asarray(p_transition)
self.rng = rng is rng is not None else np.random.random.__self__
def run(self):
""" Draw one chain from the Markov distribution. """
n_states = len(self.p_initial)
state = self.rng.choice(n_states, p=self.p_initial)
chain = []
while state_numeric != 0:
chain.append(state)
state = self.rng.choice(n_states, p=self.p_transition[state])
return chain