Mi proyecto actual, sucintamente, implica la creación de "eventos aleatorios restringidos". Básicamente estoy generando un cronograma de inspecciones. Algunos de ellos se basan en estrictas restricciones de programación; realiza una inspección una vez por semana los viernes a las 10:00 a.m. Otras inspecciones son "aleatorias"; existen requisitos configurables básicos como "una inspección debe realizarse 3 veces por semana", "la inspección debe realizarse entre las 9 AM y las 9 PM" y "no debe haber dos inspecciones dentro del mismo período de 8 horas", pero dentro de cualquier restricción que se haya configurado para un conjunto particular de inspecciones, las fechas y horas resultantes no deberían ser predecibles.
Las pruebas unitarias y TDD, IMO, tienen un gran valor en este sistema, ya que se pueden usar para construirlo de forma incremental mientras su conjunto completo de requisitos aún está incompleto, y asegúrese de que no esté "sobre-ingenierándolo" para hacer cosas que no hago. Actualmente no sé lo que necesito. Los horarios estrictos fueron pan comido para TDD. Sin embargo, me resulta difícil definir realmente lo que estoy probando cuando escribo pruebas para la parte aleatoria del sistema. Puedo afirmar que todos los tiempos producidos por el planificador deben estar dentro de las restricciones, pero podría implementar un algoritmo que pase todas esas pruebas sin que los tiempos reales sean muy "aleatorios". De hecho, eso es exactamente lo que sucedió; Encontré un problema en el que los tiempos, aunque no eran predecibles exactamente, caían en un pequeño subconjunto de los rangos de fecha / hora permitidos. El algoritmo aún pasó todas las afirmaciones que sentí que podía hacer razonablemente, y no pude diseñar una prueba automatizada que fallara en esa situación, sino que pasara cuando recibiera resultados "más aleatorios". Tuve que demostrar que el problema se resolvió mediante la reestructuración de algunas pruebas existentes para que se repitan varias veces y verifique visualmente que los tiempos generados caen dentro del rango completo permitido.
¿Alguien tiene algún consejo para diseñar pruebas que deberían esperar un comportamiento no determinista?
Gracias a todos por las sugerencias. La opinión principal parece ser que necesito una prueba determinista para obtener resultados deterministas, repetibles y afirmables . Tiene sentido.
Creé un conjunto de pruebas de "sandbox" que contienen algoritmos candidatos para el proceso de restricción (el proceso por el cual una matriz de bytes que podría ser larga se convierte en larga entre un mínimo y un máximo). Luego ejecuto ese código a través de un bucle FOR que le da al algoritmo varios conjuntos de bytes conocidos (valores de 1 a 10,000,000 solo para comenzar) y hace que el algoritmo restrinja cada uno a un valor entre 1009 y 7919 (estoy usando números primos para asegurar un el algoritmo no pasaría por un GCF fortuito entre los rangos de entrada y salida). Los valores restringidos resultantes se cuentan y se produce un histograma. Para "pasar", todas las entradas deben reflejarse dentro del histograma (cordura para garantizar que no "perdimos" ninguna), y la diferencia entre dos cubos en el histograma no puede ser mayor que 2 (realmente debería ser <= 1 , pero estad atentos). El algoritmo ganador, si lo hay, se puede cortar y pegar directamente en el código de producción y se puede establecer una prueba permanente para la regresión.
Aquí está el código:
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;
}
... y aquí están los resultados:
El rechazo de LSB (desplazamiento de bits del número hasta que cae dentro del rango) fue TERRIBLE, por una razón muy fácil de explicar; cuando divide cualquier número entre 2 hasta que sea inferior al máximo, abandona tan pronto como sea, y para cualquier rango no trivial, eso sesgará los resultados hacia el tercio superior (como se vio en los resultados detallados del histograma ) Este fue exactamente el comportamiento que vi en las fechas terminadas; Todas las horas eran en la tarde, en días muy específicos.
El rechazo de MSB (eliminar el bit más significativo del número uno a la vez hasta que esté dentro del rango) es mejor, pero nuevamente, debido a que está cortando números muy grandes con cada bit, no se distribuye uniformemente; es poco probable que obtenga números en los extremos superior e inferior, por lo que obtiene un sesgo hacia el tercio medio. Eso podría beneficiar a alguien que busca "normalizar" datos aleatorios en una curva de campana, pero una suma de dos o más números aleatorios más pequeños (similar a tirar dados) le daría una curva más natural. Para mis propósitos, falla.
El único que pasó esta prueba fue restringir por división de módulo, que también resultó ser la más rápida de las tres. El módulo, por su definición, producirá una distribución lo más uniforme posible dadas las entradas disponibles.