Meu projeto atual, sucintamente, envolve a criação de "eventos restritos e aleatórios". Estou basicamente gerando um cronograma de inspeções. Alguns deles são baseados em restrições estritas de agendamento; você realiza uma inspeção uma vez por semana na sexta-feira às 10:00. Outras inspeções são "aleatórias"; existem requisitos configuráveis básicos, como "uma inspeção deve ocorrer 3 vezes por semana", "a inspeção deve ocorrer entre as 9h e 21h" e "não deve haver duas inspeções no mesmo período de 8 horas", mas dentro de quaisquer restrições configuradas para um conjunto específico de inspeções, as datas e horários resultantes não devem ser previsíveis.
Os testes de unidade e o TDD, IMO, têm um grande valor nesse sistema, pois podem ser usados para construí-lo de forma incremental enquanto seu conjunto completo de requisitos ainda está incompleto e garantir que eu não o exagere na execução de coisas que não uso. atualmente não sei que eu preciso. Os horários rígidos eram um pedaço de bolo para o TDD. No entanto, acho difícil definir realmente o que estou testando quando escrevo testes para a parte aleatória do sistema. Posso afirmar que todos os horários produzidos pelo agendador devem estar dentro das restrições, mas eu poderia implementar um algoritmo que passe em todos esses testes sem que os horários reais sejam muito "aleatórios". De fato, foi exatamente isso que aconteceu; Encontrei um problema em que os horários, embora não exatamente previsíveis, se enquadravam em um pequeno subconjunto dos intervalos de data / hora permitidos. O algoritmo ainda passou em todas as afirmações que eu achava que poderia razoavelmente fazer, e não pude projetar um teste automatizado que falharia nessa situação, mas que passou quando obtinha resultados "mais aleatórios". Eu tive que demonstrar que o problema foi resolvido reestruturando alguns testes existentes para repetir-se várias vezes e verificar visualmente se os tempos gerados estavam dentro do intervalo permitido total.
Alguém tem alguma dica para projetar testes que devem esperar um comportamento não determinístico?
Obrigado a todos pelas sugestões. A opinião principal parece ser que eu preciso de um teste determinístico para obter resultados determinísticos, repetíveis e afirmativos . Faz sentido.
Criei um conjunto de testes "sandbox" que contêm algoritmos candidatos para o processo de restrição (o processo pelo qual uma matriz de bytes que pode ter um longo comprimento se torna um longo entre um mínimo e um máximo). Em seguida, executo esse código através de um loop FOR que fornece ao algoritmo várias matrizes de bytes conhecidas (valores de 1 a 10.000.000 apenas para iniciar) e faz com que o algoritmo restrinja cada um a um valor entre 1009 e 7919 (estou usando números primos para garantir uma algoritmo não passaria por algum GCF aleatório entre os intervalos de entrada e saída). Os valores restritos resultantes são contados e um histograma produzido. Para "passar", todas as entradas devem ser refletidas no histograma (sanidade para garantir que não "perdemos" nenhuma), e a diferença entre dois baldes no histograma não pode ser maior que 2 (na verdade deve ser <= 1 , mas fique atento). O algoritmo vencedor, se houver, pode ser cortado e colado diretamente no código de produção e um teste permanente é implementado para a regressão.
Aqui está o 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;
}
... e aqui estão os resultados:
A rejeição de LSB (mudar o número até que ele caia dentro do intervalo) foi TERRÍVEL, por um motivo muito fácil de explicar; quando você divide qualquer número por 2 até que seja menor que o máximo, você sai assim que for, e para qualquer intervalo não trivial, isso influencia os resultados para o terço superior (como foi visto nos resultados detalhados do histograma ) Esse foi exatamente o comportamento que vi nas datas finais; todos os horários eram à tarde, em dias muito específicos.
A rejeição de MSB (remover o bit mais significativo do número um de cada vez até que esteja dentro do intervalo) é melhor, mas novamente, porque você está cortando números muito grandes a cada bit, não é distribuído uniformemente; é improvável que você obtenha números nas extremidades superior e inferior, de modo a obter um viés em direção ao terço médio. Isso pode beneficiar alguém que procura "normalizar" dados aleatórios em uma curva bell-ish, mas uma soma de dois ou mais números aleatórios menores (semelhante a jogar dados) daria a você uma curva mais natural. Para os meus propósitos, falha.
O único que passou neste teste foi restringido pela divisão de módulos, que também se mostrou a mais rápida das três. O módulo, por sua definição, produzirá uma distribuição o mais uniforme possível, considerando os insumos disponíveis.