Mon projet actuel consiste, de manière succincte, à créer des "événements aléatoires contraignants". Je génère essentiellement un calendrier d'inspections. Certaines d'entre elles sont basées sur des contraintes de planning strictes; vous effectuez une inspection une fois par semaine le vendredi à 10h00. Les autres inspections sont "aléatoires"; il existe des exigences de base configurables telles que "une inspection doit avoir lieu 3 fois par semaine", "l'inspection doit avoir lieu entre 9 h et 21 h" et "il ne devrait pas y avoir deux inspections dans la même période de 8 heures", mais quelles que soient les contraintes configurées pour un ensemble particulier d’inspections, les dates et heures qui en résultent ne doivent pas être prévisibles.
Les tests unitaires et TDD, IMO, ont une grande valeur dans ce système car ils peuvent être utilisés pour le construire de manière incrémentielle alors que son ensemble complet d'exigences est encore incomplet, et assurez-vous que je ne le "sur-ingère" pas pour faire des choses que je ne fais pas. Je ne sais pas pour le moment qu'il me faut. Les horaires stricts étaient une pièce maîtresse pour TDD. Cependant, j'ai du mal à définir vraiment ce que je teste lorsque j'écris des tests pour la partie aléatoire du système. Je peux affirmer que tous les temps produits par le planificateur doivent respecter les contraintes, mais je pourrais mettre en œuvre un algorithme qui passe tous ces tests sans que les temps réels soient très "aléatoires". En fait, c'est exactement ce qui s'est passé. J'ai trouvé un problème dans lequel les heures, bien que non prévisibles exactement, entraient dans un petit sous-ensemble des plages de date / heure autorisées. L'algorithme a quand même réussi toutes les affirmations que je pouvais raisonnablement formuler et je ne pouvais pas concevoir de test automatique qui échouerait dans cette situation, mais qui réussirait s'il était donné des résultats "plus aléatoires". Je devais démontrer que le problème avait été résolu en restructurant certains tests existants pour qu'ils se répètent plusieurs fois et en vérifiant visuellement que les temps générés se situaient dans les limites de la plage autorisée.
Quelqu'un a-t-il des conseils pour concevoir des tests qui devraient s'attendre à un comportement non déterministe?
Merci à tous pour les suggestions. L’opinion principale semble être que j’ai besoin d’un test déterministe pour obtenir des résultats déterministes, répétables et assertables . Logique.
J'ai créé un ensemble de tests "bac à sable" contenant des algorithmes candidats pour le processus contraignant (processus par lequel un tableau d'octets pouvant être long devient long entre min et max). J'exécute ensuite ce code via une boucle FOR qui donne à l'algorithme plusieurs tableaux d'octets connus (valeurs comprises entre 1 et 10 000 000 juste pour commencer) et qui contraint l'algorithme à une valeur comprise entre 1009 et 7919 (j'utilise des nombres premiers pour assurer une algorithme ne passerait pas par un GCF fortuit entre les plages d’entrée et de sortie). Les valeurs contraintes résultantes sont comptées et un histogramme est généré. Pour "réussir", toutes les entrées doivent être reflétées dans l'histogramme (pour nous assurer de ne pas en "perdre"), et la différence entre deux compartiments de l'histogramme ne peut pas être supérieure à 2 (elle doit réellement être <= 1 , mais restez à l’écoute). L'algorithme gagnant, le cas échéant, peut être coupé et collé directement dans le code de production et un test permanent mis en place pour la régression.
Voici le code:
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;
}
... et voici les résultats:
Le rejet de LSB (le nombre de bits transférés jusqu’à ce qu’il se situe dans la plage) était horrible, pour une raison très facile à expliquer; lorsque vous divisez un nombre par 2 jusqu'à ce qu'il soit inférieur à un maximum, vous quittez dès qu'il est, et pour toute plage non triviale, les résultats sont biaisés vers le tiers supérieur (comme indiqué dans les résultats détaillés de l'histogramme). ) C'était exactement le comportement que j'ai vu des dates finies; toutes les heures étaient l'après-midi, des jours très spécifiques.
Le rejet de MSB (supprimer le bit le plus significatif du nombre un à la fois jusqu'à ce qu'il soit dans la plage) est préférable, mais encore une fois, étant donné que vous coupez de très grands nombres avec chaque bit, il n'est pas distribué de manière égale; il est peu probable que vous obteniez des chiffres dans les extrémités supérieure et inférieure, vous avez donc un parti pris pour le tiers central. Cela pourrait être avantageux pour quelqu'un qui cherche à "normaliser" des données aléatoires en une courbe en cloche, mais une somme de deux nombres aléatoires ou plus plus petits (similaire à un jet de dés) vous donnerait une courbe plus naturelle. Pour mes besoins, cela échoue.
Le seul qui a réussi ce test a été de contraindre par la division modulo, qui s'est également révélée être la plus rapide des trois. Selon sa définition, Modulo produira une distribution aussi uniforme que possible en fonction des entrées disponibles.