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

15

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'ils devraient être écrits, même s'ils pouvaient sembler simples et redondants. 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 en utilisant le 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ègle 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 d'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é

IS1_SO
la source
19
À quoi ressemblerait exactement un test unitaire d'une énumération?
jonrsharpe
@jonrsharpe Cela affirmerait que les valeurs qui sont à l'intérieur de l'énumération sont celles que vous attendez. Je le ferais en itérant sur les valeurs de l'énumération, en les ajoutant à un ensemble, par exemple sous forme de chaînes. Commandez cet ensemble. Le comparer cet ensemble à une liste ordonnée de valeurs écrites à la main dans le test. Ils devraient correspondre.
IS1_SO
1
@jonrsharpe, j'aime à penser que les tests unitaires sont aussi des "définitions" ou des "exigences" écrites en code. Un test unitaire d'une énumération serait aussi simple que de vérifier le nombre d'éléments sur l'énumération et leurs valeurs. Surtout en C #, où les énumérations ne sont pas des classes, mais peuvent être directement mappées à des entiers, garantir leurs valeurs et non la programmation par coïncidence peut s'avérer utile à des fins de sérialisation.
Machado
2
À mon humble avis, il n'est pas beaucoup plus utile que de tester si 2 + 2 = 4 afin de vérifier si l'Univers a raison. Vous testez le code en utilisant cette énumération, pas l'énumération elle-même.
Agent_L

Réponses:

39

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?

Non, ce sont juste des états.

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

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).

VoiceOfUnreason
la source
5
Mais 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é.
IS1_SO
4
@ IS1_SO - au point de VOU qu'un test devrait échouer cependant: l'a fait? Dans ce cas, vous n'aviez pas besoin de tester spécifiquement l'énumération. N'est-ce pas? Peut-être est un signe que vous pouvez modéliser votre code plus simple et créer une abstraction sur la « vraie nature » des données - par exemple , quelles que soient les cartes dans un jeu, avez - vous vraiment besoin d'avoir une représentation de [ Hearts, Spades, Diamonds, Clubs] si vous n'avez jamais de carte que si une carte est rouge / noire?
anotherdave
1
@ IS1_SO permet de dire que vous avez une liste de codes d'erreur et que vous souhaitez générer une null_ptrerreur. Maintenant qui a un code d'erreur via l'énumération. Le code recherchant une null_ptrerreur recherche également le code via l'énumération. Il peut donc avoir une valeur 5(par exemple). Vous devez maintenant ajouter un autre code d'erreur. L'énumération est modifiée (disons que nous en ajoutons une nouvelle en haut de l'énumération) La valeur de null_ptrest maintenant 6. Est-ce un problème? vous retournez maintenant un code d'erreur de 6et testez pour 6. Tant que tout est logiquement cohérent, tout va bien, malgré ce changement qui brise votre test théorique.
Baldrickk
17

Vous ne testez pas une déclaration d' énumération . 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 n'écrivez pas de tests vérifiant puis enum Paritydéfinit les noms Evenet 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 des tests d'écriture vérification par GetParityexemple renverront Even0, Oddpour 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 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 disposez d'un test qui garantit qu'une déclaration d' énumération définit les noms attendus, 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.

JacquesB
la source
J'ai mis à jour ma question dans le but de répondre à cette réponse, vérifiez-la pour voir si cela aide.
IS1_SO
@ IS1_SO: OK, cela m'embrouille - générez-vous dynamiquement les valeurs d'énumération, ou que se passe-t-il?
JacquesB
Non. Ce que je voulais dire, c'est que dans ce cas, la construction du langage sélectionnée pour représenter les valeurs est une énumération. Mais comme nous le savons, c'est un détail de mise en œuvre. Que se passe-t-il si l'on sélectionne un tableau, ou un Set <> (en Java) ou une chaîne avec des jetons de séparation pour représenter les valeurs? Si tel est le cas, il serait logique de tester que les valeurs contenues sont celles qui intéressent l'entreprise. C'est mon point. Cette explication est-elle utile?
IS1_SO
3
@ IS1_SO: Parlez-vous de tester une instance d'énumération renvoyée par une fonction a une certaine valeur attendue? Parce que oui, vous pouvez tester cela. Vous n'avez simplement pas besoin de tester la déclaration d'énumération elle-même.
JacquesB
11

S'il y a un risque que la modification 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.

Ian
la source
5
Si les valeurs numériques de l'énumération sont utilisées n'importe où, par exemple lorsqu'elles sont stockées dans une base de données, une réorganisation (y compris la suppression ou l'insertion avant la dernière valeur) peut entraîner la non-validité des enregistrements existants.
stannius
3
+1, cette réponse est sous-estimée. Si vos énumérations font partie de la sérialisation, de l'interface d'entrée avec un mot externe ou des informations composables au niveau du bit, elles devront certainement être testées pour la cohérence à chaque version du système. Au moins, si vous vous inquiétez de la compatibilité descendante, ce qui est généralement une bonne chose.
Machado
11

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, c'est le comportement autour de votre énumération, pas la déclaration d'énumération elle-même.

jesm00
la source
3

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 changé 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.

gnasher729
la source
1

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'est pas très utile, 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 avec elle, 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.

Paŭlo Ebermann
la source
1

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 celle voidqui ne contient que null(certaines langues appellent cela unit, et utilisent le nom voidd'une énumération sans éléments!). Il peut avoir deux valeurs, comme celle boolqui a falseet true. Il peut avoir trois, comme colourChannelavec red, greenet 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, resultcontenir win/ lose/ drawest isomorphe à ce qui précède colourChannel, car nous pouvons remplacer par exemple colourChannelpar result, redavec win, greenavec loseet blueavec 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 alors aucun changement dans notre programme. Tous les " colourChanneltests" que nous avons écrits passeront toujours, même s'il n'y en a colourChannelplus!

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. Étant donné que 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 pouvons pas simplement changer la définition; nous devons désactiver également 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). Voilà donc la seule chose qu'un «test unitaire» peut faire, par exemple

(  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 l' 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 devez 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 relèvent pas de 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 pouvons résoudre un tel problème en modifiant une énumération!

Dans de tels scénarios, l'ENUM est un hareng saur: si un système dont il a besoin d'être cette façon, et un autre , il doit être que ça? Ç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". La vraie solution est donc 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 bien 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 à 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 greenet que bluenous avons oublié de tester.
  • Les tests de mutation peuvent nous dire si nos assertions vérifient réellement l'énumération, plutôt que de simplement suivre les branches et ignorer leurs différences.
Warbo
la source
1

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 consisterait à tester 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 de documentation aussi bien que de code qui le testerait.

digimunk
la source
0

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 enum comme foo, bar... et des méthodes comme frobnicate().

Arrêtez de nuire à Monica
la source