Dans mes tests unitaires, je jette souvent des valeurs arbitraires sur mon code pour voir ce qu'il fait. Par exemple, si je sais que cela foo(1, 2, 3)
est censé renvoyer 17, je pourrais écrire ceci:
assertEqual(foo(1, 2, 3), 17)
Ces chiffres sont purement arbitraires et n’ont pas une signification plus large (ils ne sont pas, par exemple, des conditions aux limites, bien que je les vérifie également). J'aurais du mal à trouver de bons noms pour ces chiffres, et écrire quelque chose comme cela const int TWO = 2;
est évidemment inutile. Est-ce correct d'écrire les tests de cette manière, ou devrais-je factoriser les nombres en constantes?
Dans Tous les nombres magiques créés sont-ils les mêmes? , nous avons appris que les nombres magiques sont acceptables si la signification est évidente dans le contexte, mais dans ce cas, les nombres n’ont aucune signification.
la source
1, 2, 3
sont des indices de tableau 3D dans lesquels vous avez précédemment stocké la valeur17
, alors je pense que ce test serait dandy (tant que vous avez également des tests négatifs). Mais si elle est le résultat d'un calcul, vous devez vous assurer que toute personne lisant ce test comprendra pourquoifoo(1, 2, 3)
devrait être17
, et les nombres magiques ne sera probablement pas atteindre cet objectif.const int TWO = 2;
est encore pire que de simplement utiliser2
. C'est se conformer à la formulation de la règle avec l'intention de violer son esprit.foo
, cela ne signifierait rien, et donc les paramètres. Mais en réalité, je suis assez sûr que la fonction n'a pas ce nom, et les paramètres n'ont pas les nomsbar1
,bar2
etbar3
. Créez un exemple plus réaliste dans lequel les noms ont une signification. Par conséquent , il est beaucoup plus logique de déterminer si les valeurs des données de test nécessitent également un nom.Réponses:
Quand avez-vous vraiment des chiffres qui n'ont aucune signification?
Habituellement, lorsque les nombres ont une signification, vous devez les affecter à des variables locales de la méthode de test pour rendre le code plus lisible et plus explicite. Les noms des variables doivent au moins refléter ce que signifie la variable, pas nécessairement sa valeur.
Exemple:
Notez que la première variable n'est pas nommée
HUNDRED_DOLLARS_ZERO_CENT
, maisstartBalance
pour indiquer quelle est la signification de la variable mais pas que sa valeur est spéciale.la source
0.05f
unint
. :)const
variable.calculateCompoundInterest
? Si tel est le cas, la frappe supplémentaire est une preuve de travail que vous avez lue dans la documentation de la fonction que vous testez, ou au moins avez copié les noms que votre IDE vous a donnés. Je ne sais pas dans quelle mesure cela informe le lecteur de l'intention du code, mais si vous transmettez au moins les paramètres dans le mauvais ordre, ils peuvent au moins indiquer le but recherché.Si vous utilisez des nombres arbitraires uniquement pour voir ce qu'ils font, alors vous recherchez réellement des données de test générées aléatoirement, ou des tests basés sur des propriétés.
Par exemple, Hypothesis est une excellente bibliothèque Python pour ce type de test, basée sur QuickCheck .
L'idée est de ne pas vous contraindre à vos propres valeurs, mais de choisir des valeurs aléatoires qui peuvent être utilisées pour vérifier que vos fonctions correspondent à leurs spécifications. Il est important de noter que ces systèmes se souviendront généralement de toute entrée qui échouera, puis s’assureront que ces entrées seront toujours testées à l’avenir.
Le point 3 peut être déroutant pour certaines personnes, alors clarifions. Cela ne signifie pas que vous affirmez la réponse exacte - c'est évidemment impossible à faire pour une entrée arbitraire. Au lieu de cela, vous affirmez quelque chose à propos d'une propriété du résultat. Par exemple, vous pouvez affirmer qu'après l'ajout d'un élément à une liste, celui-ci devient non vide ou qu'un arbre de recherche binaire à auto-équilibrage est réellement équilibré (en utilisant les critères de cette structure de données particulière).
Dans l’ensemble, choisir des nombres arbitraires vous-même est probablement très mauvais. Cela n’ajoute pas vraiment beaucoup de valeur et est source de confusion pour quiconque le lit. Il est bon de générer automatiquement un ensemble de données de test aléatoires et de les utiliser efficacement. Trouver une hypothèse ou une bibliothèque semblable à QuickCheck pour la langue de votre choix est probablement un meilleur moyen d'atteindre vos objectifs tout en restant compréhensible pour les autres.
la source
foo
informatique) ...? Si vous êtes sûr à 100% que votre code donne la bonne réponse, il vous suffira alors de l'insérer dans le programme et de ne pas le tester. Si vous ne l'êtes pas, alors vous devez tester le test et je pense que tout le monde voit où cela se passe.d
jours, le calcul end
jours + 1 mois doit correspondre à un taux de pourcentage mensuel connu supérieur), etc.Le nom de votre test unitaire devrait fournir la majeure partie du contexte. Pas à partir des valeurs des constantes. Le nom / la documentation pour un test doit donner le contexte approprié et une explication des nombres magiques présents dans le test.
Si cela ne suffit pas, un peu de documentation devrait pouvoir la fournir (via le nom de la variable ou une chaîne de documentation). Gardez à l'esprit que la fonction elle-même a des paramètres qui, espérons-le, ont des noms significatifs. Copier ceux-ci dans votre test pour nommer les arguments est plutôt inutile.
Enfin, si vos faiblesses sont suffisamment compliquées pour que cela soit difficile / pratique, vous avez probablement trop de fonctions compliquées et vous pouvez vous demander pourquoi.
Plus vous écrivez des tests, plus votre code sera mauvais. Si vous sentez le besoin de nommer vos valeurs de test pour rendre le test clair, cela suggère fortement que votre méthode actuelle nécessite une meilleure dénomination et / ou documentation. Si vous trouvez qu'il est nécessaire de nommer les constantes dans les tests, je chercherai pourquoi vous en avez besoin - probablement, le problème n'est pas le test lui-même mais sa mise en œuvre.
la source
Cela dépend fortement de la fonction que vous testez. Je connais beaucoup de cas où les nombres individuels n'ont pas de signification particulière en soi, mais le cas de test dans son ensemble est construit de manière réfléchie et a donc une signification spécifique. C'est ce que l'on devrait documenter d'une manière ou d'une autre. Par exemple, si
foo
vraiment est une méthodetestForTriangle
qui décide si les trois nombres peuvent être des longueurs valides des arêtes d’un triangle, vos tests peuvent ressembler à ceci:etc. Vous pouvez améliorer ceci et transformer les commentaires en un paramètre de message
assertEqual
qui sera affiché en cas d'échec du test. Vous pouvez ensuite améliorer cela davantage et le transformer en un test piloté par les données (si votre framework de test le supporte). Néanmoins, vous vous faites une faveur si vous inscrivez dans le code la raison pour laquelle vous avez choisi ces chiffres et lequel des différents comportements que vous testez avec chaque cas.Bien sûr, pour d'autres fonctions, les valeurs individuelles des paramètres peuvent avoir plus d'importance. Par conséquent, utiliser un nom de fonction dépourvu de sens, comme
foo
pour demander comment traiter la signification des paramètres, n'est probablement pas la meilleure idée.la source
Pourquoi voulons-nous utiliser des constantes nommées plutôt que des nombres?
Si vous écrivez plusieurs tests unitaires, chacun avec un assortiment de 3 nombres (startBalance, intérêt, années), je mettrais simplement les valeurs dans le test unitaire en tant que variables locales. La plus petite portée à laquelle ils appartiennent.
Si vous utilisez un langage qui autorise les paramètres nommés, c'est bien sûr superflous. Là, je voudrais juste emballer les valeurs brutes dans l'appel de méthode. Je ne peux pas imaginer de refactoring rendant cette déclaration plus concise:
Ou utilisez un framework de test qui vous permettra de définir les scénarios de test dans un tableau ou un format de carte:
la source
Les nombres sont utilisés pour appeler une méthode, donc la prémisse ci-dessus est inexacte. Vous ne vous souciez peut-être pas des chiffres, mais cela n’est pas pertinent. Oui, vous pouvez en déduire à quoi servent les nombres dans certaines magies de l'IDE, mais il serait bien mieux de donner les noms de valeurs, même s'ils correspondent uniquement aux paramètres.
la source
assertEqual "Returned value" (makeKindInt 42) (runTest "lvalue_operators")
). Dans cet exemple, il42
s’agit simplement d’une valeur d’espace réservé générée par le code du script de test nommélvalue_operators
, puis vérifié lorsqu’il est renvoyé par le script. Cela n'a aucun sens, sinon que la même valeur se produit à deux endroits différents. Quel serait le nom approprié ici qui donne un sens utile?Si vous souhaitez tester une fonction pure sur un ensemble d'entrées qui ne sont pas des conditions aux limites, vous souhaiterez presque certainement la tester sur tout un ensemble d'ensembles d'entrées qui ne sont pas (et sont) des conditions aux limites. Et pour moi, cela signifie qu’il devrait y avoir une table de valeurs avec laquelle appeler la fonction et une boucle:
Des outils tels que ceux suggérés dans la réponse de Dannnno peuvent vous aider à construire la table de valeurs à tester.
bar
,baz
etblurf
devrait être remplacé par des noms significatifs, comme indiqué dans la réponse de Philipp .(Principe général discutable ici: les nombres ne sont pas toujours des "nombres magiques" qui ont besoin de noms; ils peuvent plutôt être des données . S'il est logique de placer vos nombres dans un tableau, peut-être un tableau d'enregistrements, ils sont probablement des données. Inversement, si vous pensez avoir des données sur vos mains, envisagez de les placer dans un tableau et d’en acquérir davantage.)
la source
Les tests sont différents du code de production et, du moins dans les tests unitaires écrits dans Spock, qui sont courts et précis, je n’ai aucun problème à utiliser des constantes magiques.
Si un test a une longueur de 5 lignes et suit le schéma de base donné / When / then, extraire ces valeurs en constantes ne ferait que rendre le code plus long et plus difficile à lire. Si la logique est "Quand j'ajoute un utilisateur nommé Smith, je vois que l'utilisateur Smith est retourné dans la liste des utilisateurs", l'extraction de "Smith" dans une constante est inutile.
Ceci s'applique bien entendu si vous pouvez facilement faire correspondre les valeurs utilisées dans le bloc "donné" (setup) avec celles trouvées dans les blocs "when" et "then". Si votre configuration de test est séparée (en code) de l'endroit où les données sont utilisées, il peut être préférable d'utiliser des constantes. Mais comme les tests sont mieux autonomes, la configuration est généralement proche du lieu d'utilisation et le premier cas s'applique, ce qui signifie que les constantes magiques sont tout à fait acceptables dans ce cas.
la source
Tout d'abord, convenons que le «test unitaire» est souvent utilisé pour couvrir tous les tests automatisés écrits par un programmeur, et qu'il est inutile de débattre de la manière dont chaque test devrait être appelé….
J'ai travaillé sur un système où le logiciel prenait beaucoup de données et élaborait une «solution» qui devait respecter certaines contraintes tout en optimisant d'autres nombres. Il n'y avait pas de bonne réponse, le logiciel devait donc donner une réponse raisonnable.
Il l'a fait en utilisant beaucoup de nombres aléatoires pour obtenir un point de départ, puis en utilisant un «alpiniste» pour améliorer le résultat. Cela a été couru plusieurs fois, en choisissant le meilleur résultat. Un générateur de nombres aléatoires peut être initialisé, de sorte qu'il donne toujours les mêmes nombres dans le même ordre. Par conséquent, si le test établit une valeur initiale, nous savons que le résultat sera le même à chaque exécution.
Nous avons eu beaucoup de tests qui ont fait ce qui précède, et nous avons vérifié que les résultats étaient les mêmes. Cela nous a dit que nous n'avions pas changé ce que cette partie du système avait fait par erreur lors de la refactorisation, etc. ce que cette partie du système a fait.
La maintenance de ces tests était coûteuse, car toute modification apportée au code d'optimisation aurait pour effet de rompre les tests, mais ils ont également révélé des erreurs dans le code beaucoup plus volumineux qui pré-traitait les données et post-traitait les résultats.
Lorsque nous avons «moqué» la base de données, vous pouvez appeler ces tests «tests unitaires», mais «l'unité» était plutôt grande.
Souvent, lorsque vous travaillez sur un système sans test, vous effectuez une opération similaire à celle décrite ci-dessus, de sorte que vous puissiez confirmer que votre refactoring ne modifie pas la sortie. j'espère que de meilleurs tests sont écrits pour le nouveau code!
la source
Je pense que dans ce cas, les nombres devraient être appelés des nombres arbitraires, plutôt que des nombres magiques, et commenter simplement la ligne comme "un cas test arbitraire".
Bien sûr, certains chiffres magiques peuvent aussi être arbitraires, comme pour les valeurs "handle" uniques (qui doivent être remplacées par des constantes nommées, bien sûr), mais peuvent également être des constantes précalculées du type "vitesse d'un moineau européen non chargé en tranches de quinzaine", où la valeur numérique est branchée sans commentaire ni contexte utile.
la source
Je n’irai pas jusqu’à dire un oui / non définitif, mais voici quelques questions que vous devriez vous poser lorsque vous décidez si cela vous convient ou non.
Si les chiffres ne veulent rien dire, pourquoi sont-ils là au départ? Peuvent-ils être remplacés par autre chose? Pouvez-vous effectuer une vérification basée sur des appels de méthode et un flux plutôt que sur des assertions de valeur? Considérons quelque chose comme la
verify()
méthode de Mockito qui vérifie si certains appels de méthode ont été faits pour simuler des objets au lieu d'affirmer une valeur.Si les chiffres font quelque chose de méchant, alors ils devraient être affectés à des variables qui sont nommés de façon appropriée.
L' écriture du nombre
2
queTWO
pourrait être utile dans certains contextes, et non pas tant dans d' autres contextes.assertEquals(TWO, half_of(FOUR))
cela a du sens pour quelqu'un qui lit le code. Ce que vous testez est immédiatement clair .assertEquals(numCustomersInBank(BANK_1), TWO)
, cela ne fait pas que beaucoup de sens. Pourquoi neBANK_1
contient deux clients? Que testons-nous?la source