Il mio progetto attuale, in breve, comporta la creazione di "eventi casualmente vincolanti". Fondamentalmente sto generando un programma di ispezioni. Alcuni di essi sono basati su rigidi vincoli di programma; effettui un'ispezione una volta alla settimana il venerdì alle 10:00. Altre ispezioni sono "casuali"; ci sono requisiti configurabili di base come "un'ispezione deve avvenire 3 volte a settimana", "l'ispezione deve avvenire tra le 9.00 e le 21.00" e "non ci dovrebbero essere due ispezioni entro lo stesso periodo di 8 ore", ma all'interno di qualsiasi vincolo configurato per una particolare serie di ispezioni, le date e le ore risultanti non dovrebbero essere prevedibili.
I test unitari e TDD, IMO, hanno un grande valore in questo sistema in quanto possono essere utilizzati per crearlo in modo incrementale mentre il suo set completo di requisiti è ancora incompleto, e assicurarsi che non lo stia "sovra-ingegnerizzando" cose che attualmente non so di cui ho bisogno. Gli orari rigidi erano un pezzo di torta per TDD. Tuttavia, trovo difficile definire veramente quello che sto testando quando scrivo test per la parte casuale del sistema. Posso affermare che tutte le volte prodotte dallo scheduler devono rientrare nei vincoli, ma potrei implementare un algoritmo che superi tutti questi test senza che i tempi effettivi siano molto "casuali". In realtà è esattamente quello che è successo; Ho trovato un problema in cui i tempi, sebbene non prevedibili esattamente, rientravano in un piccolo sottoinsieme degli intervalli di date / orari consentiti. L'algoritmo superava ancora tutte le asserzioni che pensavo di poter fare ragionevolmente, e non potevo progettare un test automatico che avrebbe fallito in quella situazione, ma passare quando si davano risultati "più casuali". Ho dovuto dimostrare che il problema è stato risolto ristrutturando alcuni test esistenti per ripetersi un certo numero di volte e verificando visivamente che i tempi generati rientrassero nell'intervallo consentito.
Qualcuno ha qualche consiglio per progettare test che dovrebbero aspettarsi un comportamento non deterministico?
EDIT: Grazie a tutti per i suggerimenti. L'opinione principale sembra essere che ho bisogno di un test deterministico per ottenere risultati deterministici, ripetibili e attendibili . Ha senso.
Ho creato una serie di test "sandbox" che contengono algoritmi candidati per il processo di limitazione (il processo mediante il quale un array di byte che potrebbe essere lungo può diventare un lungo tra un minimo e un massimo). Quindi eseguo quel codice attraverso un ciclo FOR che fornisce all'algoritmo diversi array di byte noti (valori da 1 a 10.000.000 solo per iniziare) e ha l'algoritmo che vincola ciascuno a un valore compreso tra 1009 e 7919 (sto usando numeri primi per garantire un l'algoritmo non passerebbe da un GCF fortuito tra gli intervalli di input e output). I valori vincolati risultanti vengono contati e viene prodotto un istogramma. Per "passare", tutti gli input devono essere riflessi all'interno dell'istogramma (sanità mentale per assicurarsi di non averli "persi") e la differenza tra due bucket nell'istogramma non può essere maggiore di 2 (dovrebbe essere davvero < = 1, ma rimanete sintonizzati). L'algoritmo vincitore, se esiste, può essere tagliato e incollato direttamente nel codice di produzione e un test permanente messo in atto per la regressione.
Ecco il codice:
private void TestConstraintAlgorithm(int min, int max, Func<byte[], long, long, long> constraintAlgorithm)
{
var histogram = new int[max-min+1];
for (int i = 1; i <= 10000000; i++)
{
//This is the stand-in for the PRNG; produces a known byte array
var buffer = BitConverter.GetBytes((long)i);
long result = constraintAlgorithm(buffer, min, max);
histogram[result - min]++;
}
var minCount = -1;
var maxCount = -1;
var total = 0;
for (int i = 0; i < histogram.Length; i++)
{
Console.WriteLine("{0}: {1}".FormatWith(i + min, histogram[i]));
if (minCount == -1 || minCount > histogram[i])
minCount = histogram[i];
if (maxCount == -1 || maxCount < histogram[i])
maxCount = histogram[i];
total += histogram[i];
}
Assert.AreEqual(10000000, total);
Assert.LessOrEqual(maxCount - minCount, 2);
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionMSBRejection()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByMSBRejection);
}
private long ConstrainByMSBRejection(byte[] buffer, long min, long max)
{
//Strip the sign bit (if any) off the most significant byte, before converting to long
buffer[buffer.Length-1] &= 0x7f;
var orig = BitConverter.ToInt64(buffer, 0);
var result = orig;
//Apply a bitmask to the value, removing the MSB on each loop until it falls in the range.
var mask = long.MaxValue;
while (result > max - min)
{
mask >>= 1;
result &= mask;
}
result += min;
return result;
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionLSBRejection()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByLSBRejection);
}
private long ConstrainByLSBRejection(byte[] buffer, long min, long max)
{
//Strip the sign bit (if any) off the most significant byte, before converting to long
buffer[buffer.Length - 1] &= 0x7f;
var orig = BitConverter.ToInt64(buffer, 0);
var result = orig;
//Bit-shift the number 1 place to the right until it falls within the range
while (result > max - min)
result >>= 1;
result += min;
return result;
}
[Test, Explicit("sandbox, does not test production code")]
public void TestRandomizerDistributionModulus()
{
TestConstraintAlgorithm(1009, 7919, ConstrainByModulo);
}
private long ConstrainByModulo(byte[] buffer, long min, long max)
{
buffer[buffer.Length - 1] &= 0x7f;
var result = BitConverter.ToInt64(buffer, 0);
//Modulo divide the value by the range to produce a value that falls within it.
result %= max - min + 1;
result += min;
return result;
}
... ed ecco i risultati:
IlrifiutodiLSB(bitchespostailnumerofinchénonrientranell'intervallo)eraTERRIBILE,perunaragionemoltofaciledaspiegare;quandodividiunnumeroqualsiasiper2finoaunvaloreinferioreaunmassimo,escinonappenaloè,eperqualsiasiintervallononbanale,questoinfluenzeràirisultativersoilterzosuperiore(comeèstatoosservatoneirisultatidettagliatidell'istogramma).Questoeraesattamenteilcomportamentochehovistodalledatefinite;tuttelevolteeranonelpomeriggio,ingiornimoltospecifici.
IlrifiutodiMSB(rimuovendoilbitpiùsignificativodalnumerounoallavoltafinchénonrientranell'intervallo)èmigliore,maancoraunavolta,poichéstaitagliandonumerimoltograndiconognibit,nonèdistribuitouniformemente;èimprobabilechesiottenganonumerinelleestremitàsuperioreeinferiore,quindisiottieneunpregiudizioversoilterzomedio.Ciòpotrebbeavvantaggiarequalcunochecercadi"normalizzare" i dati casuali in una curva campana, ma una somma di due o più numeri casuali più piccoli (simili ai dadi da lancio) ti darebbe una curva più naturale. Per i miei scopi, fallisce.
L'unico che ha superato questo test è stato vincolato dalla divisione modulo, che si è rivelata anche la più veloce delle tre. Modulo, per sua definizione, produce una distribuzione il più uniforme possibile, dati gli input disponibili.