J'essaie de me familiariser avec les tests unitaires.
Disons que nous avons un dé qui peut avoir un nombre de côtés par défaut égal à 6 (mais qui peut être à 4, 5 côtés, etc.):
import random
class Die():
def __init__(self, sides=6):
self._sides = sides
def roll(self):
return random.randint(1, self._sides)
Les tests unitaires suivants seraient-ils valides / utiles?
- tester un jet dans la gamme 1-6 pour un dé à 6 faces
- tester un jet de 0 pour un dé à 6 faces
- tester un jet de 7 pour un dé à 6 faces
- tester un jet dans la gamme 1-3 pour un dé à 3 faces
- tester un jet de 0 pour un dé à 3 faces
- tester un jet de 4 pour un dé à 3 faces
Je pense juste que c'est une perte de temps car le module aléatoire existe depuis assez longtemps, mais je pense que si le module aléatoire est mis à jour (disons que je mets à jour ma version Python), alors au moins je suis couvert.
De plus, dois-je même tester d'autres variantes de rouleaux de dé, par exemple le 3 dans ce cas, ou est-il bon de couvrir un autre état de dé initialisé?
python
unit-testing
tdd
Cybran
la source
la source
Réponses:
Vous avez raison, vos tests ne doivent pas vérifier que le
random
module fait bien son travail; un unittest ne devrait tester que la classe elle-même, pas comment elle interagit avec un autre code (qui devrait être testé séparément).Il est bien sûr tout à fait possible que votre code utilise
random.randint()
mal; ou vous appelez à larandom.randrange(1, self._sides)
place et votre dé ne lance jamais la valeur la plus élevée, mais ce serait un type de bogue différent, pas celui que vous pourriez attraper avec un plus insignifiant. Dans ce cas, votredie
appareil fonctionne comme prévu, mais la conception elle-même était défectueuse.Dans ce cas, j'utiliserais la simulation pour remplacer la
randint()
fonction, et je vérifierais seulement qu'elle a été appelée correctement. Python 3.3 et supérieur est livré avec leunittest.mock
module pour gérer ce type de test, mais vous pouvez installer lemock
package externe sur les anciennes versions pour obtenir exactement les mêmes fonctionnalitésAvec la moquerie, votre test est maintenant très simple; il n'y a que 2 cas, vraiment. Le cas par défaut pour un dé à 6 faces et le cas des côtés personnalisés.
Il existe d'autres façons de remplacer temporairement la
randint()
fonction dans l'espace de noms global deDie
, mais lemock
module rend cela plus simple. Le@mock.patch
décorateur s'applique ici à toutes les méthodes de test dans le cas de test; chaque méthode de test reçoit un argument supplémentaire, larandom.randint()
fonction simulée, afin que nous puissions tester par rapport à la maquette pour voir si elle a bien été appelée correctement. L'return_value
argument spécifie ce qui est retourné par la maquette lorsqu'elle est appelée, afin que nous puissions vérifier que ladie.roll()
méthode nous a bien renvoyé le résultat «aléatoire».J'ai utilisé une autre meilleure pratique Pitton unittesting ici: importer la classe sous test dans le cadre du test. La
_make_one
méthode effectue le travail d'importation et d'instanciation dans un test , de sorte que le module de test sera toujours chargé même si vous avez fait une erreur de syntaxe ou une autre erreur qui empêchera le module d'origine d'importer.De cette façon, si vous avez fait une erreur dans le code du module lui-même, les tests seront toujours exécutés; ils échoueront simplement, vous informant de l'erreur dans votre code.
Pour être clair, les tests ci-dessus sont extrêmement simplistes. Le but ici n'est pas de tester ce qui
random.randint()
a été appelé avec les bons arguments, par exemple. Au lieu de cela, l'objectif est de tester que l'unité produit les bons résultats compte tenu de certaines entrées, où ces entrées incluent les résultats d'autres unités non testées. En se moquant de larandom.randint()
méthode, vous pouvez prendre le contrôle d'une autre entrée de votre code.Dans les tests du monde réel , le code réel de votre unité sous test va être plus complexe; la relation avec les entrées passées à l'API et comment les autres unités sont ensuite invoquées peut être encore intéressante, et la moquerie vous donnera accès à des résultats intermédiaires, et vous permettra de définir les valeurs de retour pour ces appels.
Par exemple, dans le code qui authentifie les utilisateurs par rapport à un service OAuth2 tiers (une interaction en plusieurs étapes), vous souhaitez tester que votre code transmet les bonnes données à ce service tiers et vous permet de simuler différentes réponses d'erreur qui Le service tiers reviendrait, vous permettant de simuler différents scénarios sans avoir à créer vous-même un serveur OAuth2 complet. Ici, il est important de tester que les informations d'une première réponse ont été gérées correctement et ont été transmises à un appel de deuxième étape, vous devez donc voir que le service simulé est appelé correctement.
la source
randint()
, pas le codeDie.roll()
.sentinel.die
exemple (l'objet sentinelle vientunittest.mock
aussi), puis vérifiez qu'il s'agit bien de ce qui a été renvoyé par votre méthode roll. Cela ne permet en fait qu'une seule façon de mettre en œuvre la méthode testée.sentinel.die
serait un excellent moyen de vous en assurer.La réponse de Martijn est de savoir comment vous le feriez si vous vouliez vraiment exécuter un test qui démontre que vous appelez random.randint. Cependant, au risque qu'on me dise "cela ne répond pas à la question", je pense que cela ne devrait pas du tout être testé à l'unité. Se moquer de Randint n'est plus un test de boîte noire - vous montrez spécifiquement que certaines choses se passent dans la mise en œuvre . Le test de la boîte noire n'est même pas une option - il n'y a aucun test que vous pouvez exécuter qui prouvera que le résultat ne sera jamais inférieur à 1 ou supérieur à 6.
Pouvez-vous vous moquer
randint
? Oui, vous pouvez. Mais que prouvez-vous? Que vous l'appeliez avec les arguments 1 et côtés. Qu'est- ce que cela signifie? Vous êtes de retour à la case départ - à la fin de la journée, vous devrez prouver - formellement ou officieusement - que l'appelrandom.randint(1, sides)
met correctement en œuvre un lancer de dés.Je suis tout pour les tests unitaires. Ce sont de fantastiques tests de santé mentale et révèlent la présence de bugs. Cependant, ils ne peuvent jamais prouver leur absence, et il y a des choses qui ne peuvent pas être affirmées par le biais de tests (par exemple qu'une fonction particulière ne lève jamais d'exception ou se termine toujours.) Dans ce cas particulier, je pense qu'il y a très peu de choses que vous supportez Gain. Pour les comportements déterministes, les tests unitaires ont du sens car vous savez réellement quelle sera la réponse que vous attendez.
la source
random.randint
est appelée avec1, sides
n'a aucune valeur si ce n'est pas la bonne chose à faire.random.randint()
retournera correctement des valeurs dans la plage [1, côtés] (inclus), c'est aux développeurs de Python de s'assurer que l'random
unité fonctionne correctement.random.randint()
à vous comporter comme telrandom.randrange()
et donc à l'appeler avecrandom.randint(1, sides + 1)
, alors vous êtes de toute façon coulé.Correction d'une graine aléatoire. Pour les dés à 1, 2, 5 et 12 faces, confirmez que quelques milliers de jets donnent des résultats incluant 1 et N, et non compris 0 ou N + 1. Si, par hasard, vous obtenez un ensemble de résultats aléatoires qui ne couvrir la plage attendue, passer à une autre graine.
Les outils de moquerie sont cool, mais ce n'est pas parce qu'ils vous permettent de faire quelque chose que cela doit être fait. YAGNI s'applique autant aux appareils de test qu'aux fonctionnalités.
Si vous pouvez facilement tester avec des dépendances non simulées, vous devriez presque toujours le faire; de cette façon, vos tests se concentreront sur la réduction du nombre de défauts, et pas seulement sur l'augmentation du nombre de tests. Les moqueries excessives risquent de créer des chiffres de couverture trompeurs, ce qui peut à son tour entraîner le report des tests réels à une phase ultérieure que vous n'aurez peut-être jamais le temps de contourner ...
la source
Qu'est-ce qu'un
Die
si vous y pensez? - rien de plus qu'une enveloppe autourrandom
. Il encapsulerandom.randint
et réétiquette en termes de vocabulaire de votre application:Die.Roll
.Je ne trouve pas pertinent d'insérer une autre couche d'abstraction entre
Die
etrandom
carDie
elle-même est déjà cette couche d'indirection entre votre application et la plateforme.Si vous voulez dés en conserve des résultats, tout simplement fausse
Die
, ne vous moquez pasrandom
.En général, je ne teste pas mes objets wrapper qui communiquent avec des systèmes externes, j'écris des tests d'intégration pour eux. Vous pouvez en écrire quelques-uns,
Die
mais comme vous l'avez souligné, en raison de la nature aléatoire de l'objet sous-jacent, ils ne seront pas significatifs. De plus, aucune configuration ou communication réseau n'est impliquée ici, donc pas grand-chose à tester, sauf un appel de plate-forme.=> Considérant qu'il
Die
ne s'agit que de quelques lignes de code triviales et n'ajoutant que peu ou pas de logique par rapport àrandom
lui-même, je passerais le test dans cet exemple spécifique.la source
L'ensemencement du générateur de nombres aléatoires et la vérification des résultats attendus ne sont PAS, à ma connaissance, un test valide. Il fait des hypothèses sur la façon dont vos dés fonctionnent en interne, ce qui est vilain. Les développeurs de python pourraient changer le générateur de nombres aléatoires, ou le dé (REMARQUE: "dés" est pluriel, "mourir" est singulier. À moins que votre classe n'implémente plusieurs jets de dé en un seul appel, il devrait probablement être appelé "mourir") pourrait utilisez un générateur de nombres aléatoires différent.
De même, se moquer de la fonction aléatoire suppose que l'implémentation de classe fonctionne exactement comme prévu. Pourquoi n'est-ce pas le cas? Quelqu'un pourrait prendre le contrôle du générateur de nombres aléatoires python par défaut, et pour éviter cela, une future version de votre dé pourrait récupérer plusieurs nombres aléatoires, ou des nombres aléatoires plus grands, pour mélanger plus de données aléatoires. Un schéma similaire a été utilisé par les fabricants du système d'exploitation FreeBSD, quand ils soupçonnaient la NSA de falsifier les générateurs de nombres aléatoires matériels intégrés dans les CPU.
Si c'était moi, je lancerais, disons, 6000 rouleaux, les compterais, et m'assurerais que chaque nombre de 1 à 6 est roulé entre 500 et 1500 fois. Je vérifierais également qu'aucun numéro en dehors de cette plage n'est retourné. Je pourrais également vérifier que, pour une deuxième série de 6000 rouleaux, lors de la commande du [1..6] par ordre de fréquence, le résultat est différent (cela échouera une fois sur 720 runs, si les nombres sont aléatoires!). Si vous voulez être minutieux, vous pouvez trouver la fréquence des nombres suivant un 1, un 2, etc.; mais assurez-vous que la taille de votre échantillon est suffisamment grande et que la variance est suffisante. Les humains s'attendent à ce que les nombres aléatoires aient moins de modèles qu'ils ne le font réellement.
Répétez l'opération pour un dé à 12 faces et à 2 faces (6 est le plus utilisé, donc le plus attendu pour quiconque écrit ce code).
Enfin, je testerais pour voir ce qui se passe avec un dé à 1 face, un dé à 0 face, un dé à -1 face, un dé à 2,3 faces, un dé à [1,2,3,4,5,6] faces et un dé "bla". Bien sûr, tout cela devrait échouer; échouent-ils de manière utile? Celles-ci devraient probablement échouer à la création, pas au roulement.
Ou, peut-être, vous voulez aussi gérer cela différemment - peut-être que la création d'un dé avec [1,2,3,4,5,6] devrait être acceptable - et peut-être aussi "bla"; cela pourrait être un dé à 4 faces, et chaque face portant une lettre. Le jeu "Boggle" vient à l'esprit, tout comme une boule magique à huit.
Et enfin, vous voudrez peut-être envisager ceci: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg
la source
Au risque de nager à contre-courant, j'ai résolu ce problème exact il y a plusieurs années en utilisant une méthode pas encore mentionnée.
Ma stratégie était simplement de se moquer du RNG avec un qui produit un flux prévisible de valeurs couvrant tout l'espace. Si (disons) côté = 6 et que le RNG produit des valeurs de 0 à 5 en séquence, je peux prédire comment ma classe devrait se comporter et effectuer des tests unitaires en conséquence.
La justification est que cela teste la logique dans cette seule classe, en supposant que le RNG produira finalement chacune de ces valeurs et sans tester le RNG lui-même.
C'est simple, déterministe, reproductible et il attrape des bugs. J'utiliserais à nouveau la même stratégie.
La question ne précise pas quels devraient être les tests, mais quelles données pourraient être utilisées pour les tests, compte tenu de la présence d'un RNG. Ma suggestion est simplement de tester de manière exhaustive en se moquant du RNG. La question de savoir ce qui vaut la peine d'être testé dépend des informations non fournies dans la question.
la source
Les tests que vous proposez dans votre question ne détectent pas de compteur arithmétique modulaire comme implémentation. Et ils ne détectent pas les erreurs d'implémentation courantes dans le code lié à la distribution de probabilité comme
return 1 + (random.randint(1,maxint) % sides)
. Ou une modification du générateur qui se traduit par des motifs à deux dimensions.Si vous voulez réellement vérifier que vous générez des nombres à distribution aléatoire uniformément répartis, vous devez vérifier une très grande variété de propriétés. Pour faire un travail raisonnablement bon, vous pouvez exécuter http://www.phy.duke.edu/~rgb/General/dieharder.php sur vos numéros générés. Ou écrivez une suite de tests unitaires tout aussi complexe.
Ce n'est pas la faute des tests unitaires ou TDD, l'aléatoire est juste une propriété très difficile à vérifier. Et un sujet populaire pour des exemples.
la source
Le test le plus simple d'un jet de dé consiste simplement à le répéter plusieurs centaines de milliers de fois et à valider que chaque résultat possible a été touché environ (1 / nombre de côtés) fois. Dans le cas d'un dé à 6 faces, vous devriez voir chaque valeur possible toucher environ 16,6% du temps. Si certains sont en panne de plus d'un pour cent, vous avez un problème.
En procédant de cette façon, vous évitez de refactoriser le mécanisme sous-jacent de générer un nombre aléatoire facilement et surtout sans changer le test.
la source