web-dev-qa-db-fra.com

Test de l'unité des algorithmes inhérents aléatoires / non déterministes

Mon projet actuel, succinctement, implique la création d ' " événements constrainably aléatoires ". Je générer essentiellement un calendrier des inspections. Certains d'entre eux sont basés sur des contraintes de calendrier strict; vous effectuez une inspection une fois par semaine le vendredi à 10h00. D'autres inspections sont " au hasard "; il y a des exigences de base configurables comme " une inspection doit avoir lieu 3 fois par semaine ", " l'inspection doit avoir lieu entre les heures de 9 heures-21 heures ", et " il ne devrait pas y avoir deux inspections dans la même période de 8 heures ", mais à l'intérieur de ce que les contraintes ont été configurés pour un ensemble particulier d'inspections, les dates et les heures résultant ne devrait pas être prévisible.

Les tests unitaires et TDD, l'OMI, ont une grande valeur dans ce système, car ils peuvent être utilisés pour construire progressivement tout en son ensemble complet d'exigences est encore incomplète, et assurez-vous que je ne suis pas " over-engineering " à faire des choses je ne sais pas actuellement j'ai besoin. Les horaires stricts étaient un morceau-o'-gâteau à TDD. Cependant, je trouve qu'il est difficile de définir vraiment ce que je teste quand 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 les temps réels étant très " aléatoire ". En fait, c'est exactement ce qui est arrivé; J'ai trouvé un problème où les temps, mais pas prévisible exactement, est tombé dans un petit sous-ensemble des gammes de date/heure admissibles. L'algorithme encore passé toutes les affirmations que je sentais que je pouvais raisonnablement faire, et je ne pouvais pas concevoir un test automatisé qui échouerait dans cette situation, mais ils passent quand donné des résultats " aléatoires ". Je devais montrer le problème a été résolu par la restructuration des tests existants à se répéter plusieurs fois et vérifier visuellement que les temps générés sont tombés à l'intérieur de la plage complète admissible.

Quelqu'un at-il des conseils pour la conception de tests qui devraient 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 afin d'obtenir des résultats reproductibles, déterministes, assertable. Logique.

J'ai créé une série de tests " bac à sable " qui contiennent des algorithmes candidats pour le processus contraignant (le processus par lequel un tableau d'octets qui pourrait être tout devient long long entre min et max). Je puis exécutez ce code dans une boucle FOR qui donne l'algorithme de plusieurs tableaux d'octets connus (valeurs de 1 à 10.000.000 juste pour commencer) et l'algorithme Contraindre chacun à une valeur entre 1009 et 7919 (j'utilise des nombres premiers pour assurer une algorithme ne passerait pas par un heureux hasard GCF entre l'entrée et les plages de sortie). Les valeurs des contraintes résultantes sont comptées et un histogramme produit. Pour " passer ", toutes les entrées doivent être reflétées dans l'histogramme (santé mentale pour nous assurer ne " perdre " tout), et la différence entre les deux seaux dans l'histogramme ne peut pas être supérieure à 2 (il devrait vraiment ê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 de test mis en place permanente 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:

enter image description here

Rejet LSB (bit déplace le nombre jusqu'à ce qu'il tombe dans la région) a été TERRIBLE, pour un très facile à expliquer la raison; lorsque vous diviser un nombre par 2 jusqu'à ce qu'il soit inférieur au maximum, vous quittez dès qu'il est, et pour toute gamme non négligeable, cette volonté biaiser les résultats vers le tiers supérieur (comme on l'a vu dans les résultats détaillés de l'histogramme ). Ce fut exactement le comportement que j'ai vu des dates finies; tous les temps étaient dans l'après-midi, les jours très spécifiques.

Rejet MSB (retrait du bit le plus significatif de la première à la fois jusqu'à ce qu'il soit dans la plage) est meilleure, mais encore une fois, parce que vous êtes coupant très grand nombre à chaque bit, il est pas uniformément répartie; vous avez peu de chances d'obtenir des numéros dans les extrémités supérieure et inférieure, de sorte que vous obtenez un biais vers le troisième milieu. Cela pourrait quelqu'un avantage à la recherche de " normaliser " les données aléatoires dans une courbe en cloche-ish, mais une somme de deux ou plusieurs petits nombres aléatoires (similaire à lancer des dés) vous donnerait une courbe plus naturelle. Pour mes fins, il échoue.

Le seul qui a réussi ce test était de contraindre par la division modulo, qui a également avéré être le plus rapide des trois. Modulo sera, par définition, produire comme même une distribution possible compte tenu des entrées disponibles.

42
KeithS

Ce que vous voulez réellement tester ici, je suppose que cela compte un ensemble spécifique de résultats du Randomiser, le reste de votre méthode fonctionne correctement.

Si c'est ce que vous recherchez, alors Mock Sortir le Randomiser, pour le rendre déterministe dans les domaines du test.

J'ai généralement des objets simulés pour toutes sortes de données non déterministes ou imprévisibles (au moment de la rédaction des tests), y compris GUID générateurs et datetime.now.

Modifier, des commentaires: vous devez vous moquer de PRNG (ce terme m'a échappé la nuit dernière) au niveau le plus bas possible - c'est-à-dire. Lorsqu'il génère la gamme d'octets, pas après que vous tourniez ceux-ci INT64S. Ou même aux deux niveaux, vous pouvez donc tester votre conversion à un tableau d'œuvres INT64 comme prévu, puis testez séparément que votre conversion à un tableau de DateTimes fonctionne comme prévu. Comme le dit Jonathon, vous pouvez simplement le faire en le donnant Une graine de jeu, ou vous pouvez lui donner la gamme d'octets de retourner.

Je préfère ce dernier parce qu'il ne cassera pas si la mise en œuvre du cadre d'A PRNG changements. Cependant, un avantage de leur donner la graine est que si vous trouvez un cas dans la production qui n'a pas Travaillez comme prévu, vous n'avez besoin que d'avoir enregistré un numéro pour pouvoir le reproduire, par opposition à l'ensemble de la matrice.

Tout cela dit, vous devez vous rappeler que cela s'appelle un pseudo générateur de nombres aléatoires pour une raison. Il peut y avoir certains biais même à ce niveau.

17
pdr

Cela va sembler comme une réponse stupide, mais je vais le jeter là-bas parce que c'est comme ça que je l'ai vu fait avant:

Découplez votre code à partir du PRNG - Passez la graine de randomisation dans tout le code qui utilise la randomisation. Ensuite, vous pouvez déterminer les valeurs "Travailler" d'une seule graine (ou plusieurs graines de ce qui feraient de la sorte Vous vous sentez mieux). Cela vous donnera la capacité de tester votre code de manière adéquate sans avoir à dépendre de la loi des grands nombres.

Cela semble inuré, mais c'est ainsi que l'armée le fait (soit cela, soit-il d'une "table aléatoire" qui n'est pas vraiment aléatoire du tout)

23
Jonathan Rich

"Est-ce aléatoire (assez)" se révèle être une question incroyablement subtile. La réponse courte est qu'un test unitaire traditionnel ne le coupera tout simplement pas - vous aurez besoin de générer un tas de valeurs aléatoires et de les soumettre à divers tests statistiques qui vous donnent une grande confiance qu'ils sont suffisamment aléatoires pour vos besoins.

Il y aura un modèle - nous utilisons des générateurs de nombres pseudo-aléatoires après tout. Mais à un moment donné, les choses seront "assez bonnes" pour votre candidature (où le bien varie suffisamment entre les jeux à une extrémité, lorsque des générateurs relativement simples suffisent, jusqu'à la cryptographie où vous avez vraiment besoin de séquences pour être infaisable pour déterminer par un attaquant déterminé et bien équipé).

L'article Wikipedia http://fr.wikipedia.org/wiki/randomness_tests et ses liens de suivi ont plus d'informations.

6
James Iry

J'ai deux réponses pour vous.

=== First Réponse ===

Dès que j'ai vu le titre de votre question, je suis venu sauter et proposer la solution. Ma solution était la même que ce que plusieurs autres ont proposé: se moquer de votre générateur de nombres aléatoires. Après tout, j'ai construit plusieurs programmes différents qui nécessitaient cette astuce afin d'écrire de bons tests unitaires et j'ai commencé à accéder à un accès moqueur à des numéros aléatoires une pratique standard dans tout mon codage.

Mais alors j'ai lu votre question. Et pour le problème particulier que vous décrivez, ce n'est pas la réponse. Votre problème n'était pas que vous aviez besoin de faire un processus prévisible qui utilisait des nombres aléatoires (il serait donc essentiel). Au contraire, votre problème était de vérifier que votre algorithme a cartographié une sortie uniformément aléatoire de votre RNG à des contraintes uniformes à la sortie de votre algorithme - que si le RNG sous-jacent était uniforme, cela entraînerait une période d'inspection répartie uniformément (sous réserve de la contraintes de problème).

C'est un problème vraiment difficile (mais assez bien défini). Ce qui signifie que c'est un problème intéressant. J'ai commencé à penser à des idées de très bonnes idées pour la façon de résoudre ce problème. De retour quand j'étais un programmeur Hotshot, j'aurais peut-être commencé à faire quelque chose avec ces idées. Mais je ne suis plus un programmeur d'hotshot ... j'aime que je suis plus expérimenté et plus qualifié maintenant.

Donc, au lieu de plonger dans le problème difficile, je me suis dit: quelle est la valeur de cela? Et la réponse était décevante. Votre bogue a déjà été résolu et vous serez diligent à propos de cette question à l'avenir. Les circonstances externes ne peuvent pas déclencher le problème, change uniquement à votre algorithme. La seule raison de s'attaquer à ce problème intéressant était de satisfaire les pratiques de TDD (conception pilotée par des tests). S'il y a une chose que j'ai appris, c'est que, adhérant aveuglément à tout pratique quand ce n'est pas de précieuses causes de problèmes. Ma suggestion est la suivante: N'écrivez simplement pas de test pour cela et passez à autre chose.


=== Deuxième réponse ===

Wow ... quel problème cool!

Ce que vous devez faire ici, c'est d'écrire un test qui vérifie que votre algorithme de sélection des dates d'inspection et des temps produira une sortie uniformément distribuée (dans les contraintes de problèmes) si le RNG qu'il utilise produit des nombres uniformément distribués. Voici plusieurs approches, triées par niveau de difficulté.

  1. Vous pouvez appliquer une force brute. Il suffit de courir l'algorithme tout un tas de fois, avec un vrai rng comme entrée. Inspectez les résultats de la sortie pour voir s'ils sont distribués uniformément. Votre test devra échouer si la distribution varie de parfaitement uniforme par plus d'un certain bathhold et de vous assurer de capturer des problèmes que le seuil ne peut pas être réglé trop bas. Cela signifie que vous aurez besoin d'un grand nombre de points afin d'être sûr que la probabilité d'un faux positif (une défaillance de test par hasard) est très faible (bien <1% pour une base de code de taille moyenne; encore moins pour une grosse base de code).

  2. Considérez votre algorithme en tant que fonction qui prend la concaténation de la totalité de la sortie RNG comme entrée, puis produit des temps d'inspection sous forme de sortie. Si vous savez que cette fonction est continue par morceaux, il existe alors un moyen de tester votre propriété. Remplacez le RNG avec un RNG moquable et exécutez l'algorithme à plusieurs reprises, produisant une sortie RNG distribuée uniformément. Donc, si votre code a besoin de 2 appels RNG, chacun dans la plage [0..1], vous pourriez avoir le test d'exécuter l'algorithme 100 fois, renvoyer les valeurs [(0,0,0,0), (0,0,0,1), (0,0, 0,2), ... (0,0,0,9), (0,1,0,0), (0,1,0,1), ... (0,9,0,9)]. Ensuite, vous pouvez vérifier si la sortie des 100 exécutions était (environ) répartie d'uniformément dans la plage autorisée.

  3. Si vous avez vraiment besoin de vérifier l'algorithme de manière fiable et que vous ne pouvez pas faire des hypothèses sur l'algorithme OR Exécutez un grand nombre de fois, vous pouvez toujours attaquer le problème, mais vous pourriez Besoin de contraintes sur la façon dont vous programmez l'algorithme. Vérifiez pypy et leur espace d'objet Approche comme exemple. Vous pouvez créer un espace d'objet qui, au lieu de Executation L'algorithme, il suffit de calculer la forme de la distribution de sortie (supposant que l'entrée RNG est uniforme). Bien entendu, cela nécessite que vous construisiez un tel outil et que votre algorithme soit construit en Pypy ou à un autre outil où il est facile de faire des modifications radicales au compilateur et de l'utiliser pour analyser le code.

5
mcherm

Vous voudrez peut-être jeter un coup d'œil à Sevcikova et al: "Test automatisé de systèmes stochastiques: une approche statistique fondée" ( [~ # ~] pdf [~ # ~] ).

La méthodologie est mise en œuvre dans divers étuis de test pour la plate-forme de simulation Urbansim .

3
krlmlr

Pour les tests de l'unité, remplacez le générateur aléatoire avec une classe qui génère des résultats prévisibles couvrant tous les cas d'angle. C'est à dire. Assurez-vous que votre pseudo-randomiseur génère la valeur la plus basse possible et la valeur la plus longue possible, et le même résultat à plusieurs reprises d'une rangée.

Vous ne voulez pas que vos tests de l'unité donnent sur E.G. Les bugs hors-tête se produisent lorsque Random.Nextint (1000) retourne 0 ou 999.

3
user281377

Une approche d'histogramme simple est une bonne première étape, mais ne suffit pas à prouver au hasard. Pour un uniforme PRNG Vous seriez également (au moins) générer un tracé de dispersion à 2 dimensions (où X est la valeur précédente et y est la nouvelle valeur). Cette parcelle doit également être uniforme. . Ceci est compliqué dans votre situation car il existe des non-linéarités intentionnelles dans le système.

Mon approche serait:

  1. valider (ou prendre comme indiqué) que la source PRNG est suffisamment aléatoire (à l'aide de mesures statistiques standard)
  2. vérifiez qu'une conversion de PRNG-to-DateTime non contrainte est suffisamment aléatoire sur l'espace de production (cela vérifie un manque de biais dans la conversion). Votre test d'uniformité de premier ordre simple devrait être suffisant ici.
  3. vérifiez que les cas contraignés sont suffisamment uniformes (un simple test d'uniformité de premier ordre sur les bacs valides).

Chacun de ces tests est statistique et nécessite un grand nombre de points d'échantillonnage pour éviter les faux positifs et les faux négatifs avec un degré de confiance élevé.

Quant à la nature de l'algorithme de conversion/contrainte:

donné : méthode pour générer une valeur pseudo-aléatoire p où 0 <= p <= [~ # ~ # ~] m [~ # ~]

besoin : sortie y dans la plage (éventuellement discontinue) 0 <= y <= [~ # ~ # ~] n [~ # ~] <= [ ~ # ~] m [~ # ~]

Algorithme :

  1. calculez r = floor(M / N), c'est-à-dire le nombre de gammes de sortie complètes qui s'adaptent à la plage d'entrée.
  2. calculez la valeur maximale acceptable pour P : p_max = r * N
  3. générez des valeurs pour p jusqu'à une valeur inférieure ou égale à p_max est trouvé
  4. calculer y = p / r
  5. si y est acceptable, renvoyez-la, sinon répétez avec l'étape 3

La clé est de supprimer des valeurs inacceptables plutôt que de plier de manière non uniforme.

en pseudo-code:

# assume prng generates non-negative values
def randomInRange(min, max, prng):
    range = max - min
    factor = prng.max / range

    do:
        value = prng()
    while value > range * factor
    return (value / factor) + min

def constrainedRandom(constraint, prng):
    do:
        value = randomInRange(constraint.min, constraint.max, prng)
    while not constraint.is_acceptable(value)
2
Frank Szczerba

En plus de valider que votre code n'échoue pas, ou jette des exceptions droites dans des endroits de droite, vous pouvez créer une paire d'entrée/réponse valide (même le calculant manuellement), alimentez l'entrée dans le test et assurez-vous qu'il retourne la réponse attendue. Pas génial, mais c'est à peu près tout ce que tu peux faire, Imho. Cependant, dans votre cas, ce n'est pas vraiment aléatoire, une fois que vous avez créé votre horaire, vous pouvez tester la conformité de la règle - doit avoir 3 inspections par semaine, entre 9 et 9; Il n'y a pas de besoin réel ou de capacité à tester pour des moments exacts lorsque l'inspection s'est produite.

1
Evgeni

Il n'y a vraiment pas de meilleur moyen que de courir un tas de fois et de voir si vous obtenez la distribution que vous voulez. Si vous avez 50 calendriers d'inspection potentiels autorisés, vous exécutez le test 500 fois et assurez-vous que chaque planification est utilisée à proximité de 10 fois. Vous pouvez contrôler vos graines de générateur aléatoire pour le rendre plus déterministe, mais cela rendra également vos tests plus étroitement couplés aux détails de la mise en œuvre.

1
Karl Bielefeldt

Il n'est pas possible de tester une condition nébuleuse qui n'a pas de définition concrète. Si les dates générées transmettent tous les tests, votre application fonctionne correctement avec théorie. L'ordinateur ne peut pas vous dire si les dates sont "assez aléatoires" car il ne peut pas accuser réception des critères d'un tel test. Si tous les tests passent, mais le comportement de l'application ne convient toujours pas, votre couverture de test est inadéquate de manière empirique (à partir d'une perspective TDD).

À mon avis, votre meilleur est de mettre en œuvre des contraintes de génération de date arbitraire afin que la distribution passe un test d'odeur humain.

1
leo

Ce cas semble idéal pour Test basé sur la propriété .

En un mot, il s'agit d'un mode de test lorsque la structure de test génère des entrées pour le code dans le test et des assertions de test Valider Propriétés des sorties. Le cadre peut ensuite être suffisamment intelligent pour "attaquer" le code à tester et essayer de le corporer dans une erreur. Le cadre est généralement assez intelligent pour détourner votre graine de générateur de nombres aléatoires. Généralement, vous pouvez configurer le cadre pour générer au plus N cas de test ou exécuter au maximum N secondes et rappelez-vous que les cas de test ont échoué de la dernière exécution et de réexécuteront d'abord ceux-ci. Cela permet un cycle d'itération rapide pendant le développement et le test ralentissement complet de la bande/en CI.

Voici un exemple (muet, échouant) testant la fonction sum:

@given(lists(floats()))
def test_sum(alist):
    result = sum(alist)
    assert isinstance(result, float)
    assert result > 0
  • le cadre de test générera une liste à la fois
  • le contenu de la liste sera des chiffres de points flottants
  • sum est appelé et les propriétés du résultat sont validées
  • le résultat flotte toujours
  • le résultat est positif

Ce test trouvera un tas de "bugs" dans sum (commentaire si vous pouviez deviner tout Par vous-même):

  • sum([]) is 0 (int, pas un flotteur)
  • sum([-0.9]) est négatif
  • sum([0.0]) n'est pas strictement positif
  • sum([..., nan]) is nan qui n'est pas positif

Avec Paramètres par défaut, hpythesis abrite le test après une entrée "mauvaise" est trouvé, ce qui est bon pour TDD. Je pensais qu'il était possible de la configurer pour signaler plusieurs/toutes les entrées "mauvaises", mais je ne trouve pas ces options maintenant.

Dans le cas de l'OP, les propriétés validées seront plus complexes: type d'inspection Un présent, type d'inspection A trois fois par semaine, heure d'inspection B toujours à 12 heures, type C de 9 à 9, [calendrier donné une semaine] Inspections de types A, b, c tous présents, etc.

La bibliothèque la plus connue est QuickCheck pour Haskell, voir la page Wikipedia ci-dessous pour une liste de telles bibliothèques dans d'autres langues:

https://fr.wikipedia.org/wiki/quickcheck

Hypothèse (pour Python) a une bonne rédaction sur ce type de test:

https://hypothèse.works/articles/what-is-property-based-testing/

0
Dima Tisnek

Il suffit d'enregistrer la sortie de votre randomizer (que ce soit pseudo ou quantique/monde chaotique ou réel). Ensuite, enregistrez et rejouez ces séquences "aléatoires" adaptées à vos exigences de test, ou qui exposent des problèmes et des bogues potentiels, lorsque vous construisez vos cas de test de l'unité.

0
hotpaw2