web-dev-qa-db-fra.com

Doit-on tester les valeurs d'une énumération à l'aide de tests unitaires?

Si vous avez une énumération avec des valeurs uniquement (aucune méthode comme on pourrait le faire en Java), et cette énumération fait partie de la définition métier du système, doit-on écrire des tests unitaires pour cela?

Je pensais qu'elles devraient être écrites, même si elles pouvaient sembler simples et redondantes, je considère que ce qui concerne la spécification métier devrait être explicitement écrit dans un test, que ce soit avec unit/integration/ui/etc. tests ou en utilisant le système de type de la langue comme méthode de test. Étant donné que les valeurs qu'une énumération (par exemple en Java) doit avoir, du point de vue de l'entreprise, ne peuvent pas être testées à l'aide du système de type, je pense qu'il devrait y avoir un test unitaire pour cela.

Cette question n'est pas similaire à celle-ci car elle ne résout pas le même problème que le mien. Dans cette question, il y a une fonction métier (savePeople) et la personne se renseigne sur l'implémentation interne (forEach). Là-bas, il y a une couche métier intermédiaire (la fonction sauver les gens) encapsulant la construction du langage (forEach). Ici, la construction du langage (enum) est celle utilisée pour spécifier le comportement d'un point de vue commercial.

Dans ce cas, le détail de l'implémentation coïncide avec la "vraie nature" des données, c'est-à-dire un ensemble (au sens mathématique) de valeurs. On pourrait sans doute utiliser un ensemble immuable, mais les mêmes valeurs devraient toujours y être présentes. Si vous utilisez un tableau, la même chose doit être effectuée pour tester la logique métier. Je pense que l'énigme ici est le fait que la construction du langage coïncide très bien avec la nature des données. Je ne sais pas si je me suis bien expliqué

16
IS1_SO

Si vous avez une énumération avec des valeurs uniquement (pas de méthode comme on pourrait le faire en Java), et cette énumération fait partie de la définition métier du système, doit-on écrire des tests unitaires pour cela?

Non, ce sont juste des états.

Fondamentalement, le fait que vous utilisez une énumération est un détail d'implémentation; c'est le genre de chose que vous voudrez peut-être réorganiser dans une conception différente.

Tester l'énumération de l'exhaustivité est analogue à tester que tous les entiers représentables sont présents.

Cependant, tester les comportements pris en charge par les énumérations est une bonne idée. En d'autres termes, si vous partez d'une suite de tests réussie et commentez une valeur d'énumération unique, alors au moins un test doit échouer (les erreurs de compilation étant considérées comme des échecs).

40
VoiceOfUnreason

Vous ne testez pas une énumération déclaration. Vous pouvez tester si l'entrée/sortie de fonction a les valeurs d'énumération attendues. Exemple:

enum Parity {
    Even,
    Odd
}

Parity GetParity(int x) { ... }

Vous ne pas écrivez des tests vérifiant puis enum Parity définit les noms Even et Odd. Un tel test serait inutile car vous répéteriez simplement ce qui est déjà indiqué par le code. Dire deux fois la même chose ne le rend pas plus correct.

Vous faites écrivez des tests vérifiant GetParity say renverra Even pour 0, Odd pour 1 et ainsi de suite. Ceci est précieux car vous ne répétez pas le code, vous vérifiez le comportement du code, indépendamment de l'implémentation. Si le code à l'intérieur de GetParity était complètement réécrit, les tests seraient toujours valides. En effet, les principaux avantages des tests unitaires sont qu'ils vous donnent la liberté de réécrire et de refactoriser le code en toute sécurité, en garantissant que le code fonctionne toujours comme prévu.

Mais si vous avez un test qui garantit qu'une énumération déclaration définit les noms attendus, alors toute modification que vous apporterez à l'énumération à l'avenir vous obligera à modifier également le test. Cela signifie que ce n'est pas seulement deux fois plus de travail, cela signifie également que tout avantage du test unitaire est perdu. Si vous devez changer de code et tester à en même temps, il n'y a aucune garantie contre l'introduction de bogues.

17
JacquesB

S'il y a un risque que le changement de l'énumération casse votre code, alors bien sûr, tout ce qui a l'attribut [Flags] en C # serait un bon cas car l'ajout d'une valeur entre 2 et 4 (3) serait un 1 et 2 au niveau du bit plutôt qu'un article discret.

C'est une couche de protection.

Vous devriez envisager d'avoir un code de pratique enum que tous les développeurs connaissent. Ne vous fiez pas aux représentations textuelles de l'énumération qui est courante, mais cela pourrait entrer en conflit avec vos directives de sérialisation.

J'ai vu des gens "corriger" la mise en majuscule des entrées d'énumération, les trier par ordre alphabétique ou par un autre regroupement logique qui ont tous cassé d'autres morceaux de mauvais code.

11
Ian

Non, un test vérifiant qu'une énumération contient toutes les valeurs valides et rien de plus répète essentiellement la déclaration de l'énumération. Vous ne feriez que tester que le langage implémente correctement la construction enum qui est un test insensé.

Cela étant dit, vous devez tester le comportement qui dépend des valeurs d'énumération. Par exemple, si vous utilisez les valeurs d'énumération pour sérialiser des entités en json ou autre, ou si vous stockez les valeurs dans une base de données, vous devez tester le comportement de toutes les valeurs de l'énumération. De cette façon, si l'énumération est modifiée, au moins l'un des tests devrait échouer. Dans tous les cas, ce que vous testeriez est le comportement autour de votre énumération, pas la déclaration d'énumération elle-même.

11
jesm00

Votre code devrait fonctionner correctement indépendamment des valeurs réelles d'une énumération. Si tel est le cas, aucun test unitaire n'est nécessaire.

Mais vous pouvez avoir du code où la modification d'une valeur d'énumération va casser les choses. Par exemple, si une valeur d'énumération est stockée dans un fichier externe, et après avoir modifié la valeur d'énumération, la lecture du fichier externe donnera le mauvais résultat. Dans ce cas, vous aurez un GRAND commentaire près de l'énumération avertissant quiconque de ne modifier aucune valeur, et vous pouvez très bien écrire un test unitaire qui vérifie les valeurs numériques.

3
gnasher729

En général, le simple fait de vérifier qu'une énumération possède une liste de valeurs codées en dur n'a pas beaucoup de valeur, comme l'ont dit d'autres réponses, car il suffit alors de mettre à jour test et énumération ensemble.

J'ai eu une fois un cas où un module utilisait des types d'énumération de deux autres modules et mappés entre eux. (L'une des énumérations avait une logique supplémentaire, l'autre était pour l'accès DB, les deux avaient des dépendances qui devraient être isolées les unes des autres.)

Dans ce cas, j'ai ajouté un test (dans le module de mappage) qui a vérifié que toutes les entrées d'énumération dans l'énumération source existent également dans l'énumération cible (et donc, que le mappage fonctionnerait toujours). (Dans certains cas, j'ai également vérifié l'inverse.)

De cette façon, lorsque quelqu'un a ajouté une entrée d'énumération à l'une des énumérations et a oublié d'ajouter l'entrée correspondante à l'autre, un test a commencé à échouer.

1
Paŭlo Ebermann

Les énumérations sont simplement des types finis, avec des noms personnalisés (espérons-le significatifs). Une énumération peut n'avoir qu'une seule valeur, comme void qui ne contient que null (certaines langues appellent cela unit, et utilisent le nom void pour une énumération avec non éléments!). Il peut avoir deux valeurs, comme bool qui a false et true. Il peut en avoir trois, comme colourChannel avec red, green et blue. Etc.

Si deux énumérations ont le même nombre de valeurs, elles sont alors "isomorphes"; c'est-à-dire que si nous supprimons systématiquement tous les noms, nous pouvons en utiliser un à la place d'un autre et notre programme ne se comportera pas différemment. En particulier, nos tests ne se comporteront pas différemment!

Par exemple, result contenant win/lose/draw est isomorphe à ce qui précède colourChannel, puisque nous pouvons remplacer par ex. colourChannel avec result, red avec win, green avec lose et blue avec draw, et tant que nous le faisons partout (producteurs et consommateurs, analyseurs et sérialiseurs, entrées de base de données, fichiers journaux, etc.), il n'y aura aucun changement dans notre programme. Tous les "colourChannel tests" que nous avons écrits passeront toujours, même s'il n'y a plus de colourChannel!

De plus, si une énumération contient plusieurs valeurs, nous pouvons toujours réorganiser ces valeurs pour obtenir une nouvelle énumération avec le même nombre de valeurs. Puisque le nombre de valeurs n'a pas changé, le nouvel arrangement est isomorphe à l'ancien, et donc nous pourrions changer tous les noms et nos tests passeraient toujours (notez que nous ne peut pas juste désactiver la définition; nous devons également désactiver tous les sites d'utilisation).

Cela signifie que, en ce qui concerne la machine, les énumérations sont des "noms distinctifs" et rien d'autre. La seule chose que nous pouvons faire avec une énumération est de déterminer si deux valeurs sont identiques (par exemple red/red) ou différentes (par exemple red/blue). C'est donc la seule chose qu'un `` test unitaire '' puisse faire, par ex.

(  red == red  ) || throw TestFailure;
(green == green) || throw TestFailure;
( blue == blue ) || throw TestFailure;
(  red != green) || throw TestFailure;
(  red != blue ) || throw TestFailure;
...

Comme le dit @ jesm00, un tel test vérifie le implémentation du langage plutôt que votre programme. Ces tests ne sont jamais une bonne idée: même si vous ne faites pas confiance à l'implémentation du langage, vous devriez le tester de l'extérieur, car il ne peut pas faire confiance pour exécuter les tests correctement!

Voilà donc la théorie; qu'en est-il de la pratique? Le principal problème avec cette caractérisation des énumérations est que les programmes du `` monde réel '' sont rarement autonomes: nous avons des versions héritées, des déploiements à distance/intégrés, des données historiques, des sauvegardes, des bases de données en direct, etc. donc nous ne pouvons jamais vraiment `` basculer '' toutes les occurrences d'un nom sans manquer certaines utilisations.

Pourtant, de telles choses ne sont pas la "responsabilité" de l'énumération elle-même: la modification d'une énumération peut interrompre la communication avec un système distant, mais inversement, nous pourrions corriger un tel problème en modifiant une énumération!

Dans de tels scénarios, l'énumération est un redingue: que se passe-t-il si un système a besoin que ce soit this way, et un autre a besoin qu'il soit that way? Ça ne peut pas être les deux, peu importe le nombre de tests que nous écrivons! Le vrai coupable ici est l'interface d'entrée/sortie, qui devrait produire/consommer des formats bien définis plutôt que "quel que soit l'entier choisi par l'interprète". Donc, la vraie solution est de tester les interfaces d'E/S: avec des tests unitaires pour vérifier qu'il analyse/imprime le format attendu, et avec des tests d'intégration pour vérifier que le format est réellement accepté par l'autre côté .

Nous pouvons encore nous demander si l'énumération est "suffisamment exercée", mais dans ce cas, l'énumération est à nouveau un hareng rouge. Ce qui nous préoccupe réellement, c'est la suite de tests elle-même. Nous pouvons gagner en confiance ici de deux manières:

  • La couverture du code peut nous dire si la variété des valeurs d'énumération provenant de la suite de tests est suffisante pour déclencher les différentes branches du code. Sinon, nous pouvons ajouter des tests qui déclenchent les branches découvertes, ou générer une plus grande variété d'énumérations dans les tests existants.
  • La vérification des propriétés peut nous dire si la variété des branches dans le code est suffisante pour gérer les possibilités d'exécution. Par exemple, si le code ne gère que red et que nous testons uniquement avec red, alors nous avons une couverture de 100%. Un vérificateur de propriétés va (essayer de) générer des contre-exemples à nos assertions, comme générer les valeurs green et blue que nous avons oublié de tester.
  • Les tests de mutation peuvent nous dire si nos affirmations sont réellement check l'énumération, plutôt que de simplement suivre les branches et ignorer leurs différences.
1
Warbo

Les tests unitaires concernent les unités de test.

Dans la programmation orientée objet, une unité est souvent une interface entière, telle qu'une classe, mais pourrait être une méthode individuelle.

https://en.wikipedia.org/wiki/Unit_testing

Un test automatisé pour une énumération déclarée testerait l'intégrité du langage et de la plate-forme sur laquelle il s'exécute plutôt que la logique dans du code créé par le développeur. Cela ne servirait à rien - documentation incluse puisque le code déclarant l'énumération sert tout autant de documentation que de code qui le testerait.

1
digimunk

Vous devez tester le comportement observable de votre code, les effets des appels de méthode/fonction sur l'état observable. Tant que le code fait la bonne chose, tout va bien, vous n'avez pas besoin de tester autre chose.

Vous n'avez pas besoin d'affirmer explicitement qu'un type d'énumération a les entrées que vous attendez, tout comme vous n'affirmez pas explicitement qu'une classe existe réellement ou qu'elle a les méthodes et les attributs que vous attendez.

En fait, en testant le comportement, vous affirmez implicitement que les classes, méthodes et valeurs impliquées dans le test existent, vous n'avez donc pas besoin de l'affirmer explicitement.

Notez que vous n'avez pas besoin de noms significatifs pour que votre code fasse la bonne chose, c'est juste une commodité pour les personnes qui lisent votre code. Vous pouvez faire fonctionner votre code avec des valeurs d'énumération comme foo, bar... et des méthodes comme frobnicate().

0
Stop harming Monica