Il mio attuale progetto, in breve, prevede la creazione di "eventi casualmente vincolanti". Fondamentalmente sto generando un programma di ispezioni. Alcuni di essi si basano su rigidi vincoli di pianificazione; esegui un'ispezione una volta alla settimana il venerdì alle 10:00. Altre ispezioni sono "casuali"; ci sono requisiti di base configurabili come "un'ispezione deve avvenire 3 volte a settimana", "l'ispezione deve avvenire tra le 9:00 e le 21:00" e "non devono esserci due ispezioni nello stesso periodo di 8 ore", ma all'interno di qualunque vincolo sia stato configurato per un determinato insieme di ispezioni, le date e gli orari risultanti non dovrebbero essere prevedibili.
I test unitari e TDD, IMO, hanno un grande valore in questo sistema in quanto possono essere utilizzati per costruirlo in modo incrementale mentre il suo set completo di requisiti è ancora incompleto e assicurarsi che non lo stia "progettando troppo" per fare le cose che faccio attualmente so che ho bisogno. I rigidi programmi erano un pezzo per TDD. Tuttavia, trovo difficile definire veramente ciò che sto testando quando scrivo test per la parte casuale del sistema. Posso affermare che tutti i tempi prodotti dallo scheduler devono rientrare nei vincoli, ma potrei implementare un algoritmo che supera tutti questi test senza che i tempi effettivi siano molto "casuali". In realtà è esattamente quello che è successo; Ho riscontrato un problema in cui i tempi, sebbene non prevedibili esattamente, rientravano in un piccolo sottoinsieme degli intervalli di data / ora consentiti. L'algoritmo superava ancora tutte le affermazioni che ritenevo di poter ragionevolmente fare e non potevo progettare un test automatizzato che avrebbe fallito in quella situazione, ma che avrebbe superato risultati "più casuali". Ho dovuto dimostrare che il problema è stato risolto ristrutturando alcuni test esistenti per ripetersi più volte e verificare visivamente che i tempi generati rientrassero nell'intero intervallo consentito.
Qualcuno ha qualche suggerimento per la progettazione di test che dovrebbero aspettarsi comportamenti non deterministici?
Grazie a tutti per i suggerimenti. L'opinione principale sembra essere che ho bisogno di un test deterministico per ottenere risultati deterministici, ripetibili, asseribili . Ha senso.
Ho creato una serie di test "sandbox" che contengono algoritmi candidati per il processo vincolante (il processo mediante il quale un array di byte che potrebbe essere lungo diventa 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 all'inizio) e ha l'algoritmo vincolato a un valore compreso tra 1009 e 7919 (sto usando numeri primi per garantire un l'algoritmo non passerebbe da alcuni GCF casuali tra gli intervalli di input e output). Vengono contati i valori vincolati risultanti e viene prodotto un istogramma. Per "passare", tutti gli input devono essere riflessi nell'istogramma (sanità mentale per garantire che non abbiamo "perso" alcuno) e la differenza tra due secchi nell'istogramma non può essere maggiore di 2 (dovrebbe essere <= 1 , ma rimanete sintonizzati). L'algoritmo vincente, se presente, può essere tagliato e incollato direttamente nel codice di produzione e messo a punto un test permanente 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:
Il rifiuto dell'LSB (spostando bit il numero fino a quando non rientra nell'intervallo) era TERRIBILE, per una ragione molto facile da spiegare; quando dividi qualsiasi numero per 2 fino a quando è inferiore a un massimo, esci non appena lo è, e per qualsiasi intervallo non banale, che distorcerà i risultati verso il terzo superiore (come si è visto nei risultati dettagliati dell'istogramma ). Questo era esattamente il comportamento che ho visto dalle date finite; tutte le volte erano nel pomeriggio, in giorni molto specifici.
Il rifiuto di MSB (rimuovendo il bit più significativo dal numero uno alla volta fino a quando non rientra nell'intervallo) è meglio, ma ancora una volta, poiché stai tagliando numeri molto grandi con ogni bit, non è distribuito uniformemente; è improbabile che si ottengano numeri nelle estremità superiore e inferiore, quindi si ottiene un orientamento verso il terzo medio. Ciò potrebbe giovare a qualcuno che cerca di "normalizzare" i dati casuali in una curva a campana, ma una somma di due o più numeri casuali più piccoli (simile al lancio di dadi) ti darebbe una curva più naturale. Per i miei scopi, fallisce.
L'unico che ha superato questo test è stato quello di limitare la divisione del modulo, che si è rivelata anche la più veloce delle tre. Modulo, per definizione, produrrà il più uniforme possibile una distribuzione dati gli input disponibili.