Le code dupliqué est-il plus tolérable dans les tests unitaires?

113

J'ai gâché plusieurs tests unitaires il y a quelque temps lorsque je les ai refactorisés pour les rendre plus SECS - l'intention de chaque test n'était plus claire. Il semble qu'il y ait un compromis entre la lisibilité et la maintenabilité des tests. Si je laisse du code dupliqué dans les tests unitaires, ils sont plus lisibles, mais si je change le SUT , je devrai traquer et changer chaque copie du code dupliqué.

Êtes-vous d'accord pour dire que ce compromis existe? Dans l'affirmative, préférez-vous que vos tests soient lisibles ou maintenables?

Daryl Spitzer
la source

Réponses:

68

Le code dupliqué est une odeur dans le code de test unitaire autant que dans un autre code. Si vous avez du code dupliqué dans les tests, il est plus difficile de refactoriser le code d'implémentation car vous avez un nombre disproportionné de tests à mettre à jour. Les tests doivent vous aider à refactoriser en toute confiance, plutôt que d'être un lourd fardeau qui empêche votre travail sur le code testé.

Si la duplication est en cours de configuration, envisagez d'utiliser davantage la setUpméthode ou de fournir des méthodes de création plus (ou plus flexibles) .

Si la duplication est dans le code manipulant le SUT, alors demandez-vous pourquoi plusieurs tests dits «unitaires» exercent exactement la même fonctionnalité.

Si la duplication se trouve dans les assertions, vous avez peut-être besoin d' assertions personnalisées . Par exemple, si plusieurs tests ont une chaîne d'assertions comme:

assertEqual('Joe', person.getFirstName())
assertEqual('Bloggs', person.getLastName())
assertEqual(23, person.getAge())

Alors peut-être avez-vous besoin d'une seule assertPersonEqualméthode pour pouvoir écrire assertPersonEqual(Person('Joe', 'Bloggs', 23), person). (Ou peut-être avez-vous simplement besoin de surcharger l'opérateur d'égalité Person.)

Comme vous l'avez mentionné, il est important que le code de test soit lisible. En particulier, il est important que l' intention d'un test soit claire. Je trouve que si de nombreux tests se ressemblent pour la plupart (par exemple, les trois quarts des lignes sont identiques ou pratiquement identiques), il est difficile de repérer et de reconnaître les différences significatives sans les lire attentivement et les comparer. Je trouve donc que la refactorisation pour supprimer la duplication contribue à la lisibilité, car chaque ligne de chaque méthode de test est directement pertinente par rapport à l'objectif du test. C'est beaucoup plus utile pour le lecteur qu'une combinaison aléatoire de lignes directement pertinentes et de lignes qui ne sont que des passe-partout.

Cela dit, les tests exercent parfois des situations complexes qui sont similaires mais toujours significativement différentes, et il est difficile de trouver un bon moyen de réduire la duplication. Faites preuve de bon sens: si vous pensez que les tests sont lisibles et que leur intention est claire, et que vous êtes à l'aise avec le fait d'avoir peut-être besoin de mettre à jour plus qu'un nombre théoriquement minimal de tests lors de la refactorisation du code invoqué par les tests, acceptez l'imperfection et déplacez-vous sur quelque chose de plus productif. Vous pouvez toujours revenir et refactoriser les tests plus tard, lorsque l'inspiration vous frappe!

spiv
la source
30
"Le code dupliqué est une odeur dans le code de test unitaire autant que dans un autre code." "Si vous avez du code dupliqué dans les tests, il est plus difficile de refactoriser le code d'implémentation car vous avez un nombre disproportionné de tests à mettre à jour." Cela se produit parce que vous testez l'API privée au lieu de l'API publique.
15
Mais pour éviter le code dupliqué dans les tests unitaires, vous devez généralement introduire une nouvelle logique. Je ne pense pas que les tests unitaires devraient contenir de la logique, car vous auriez alors besoin de tests unitaires de tests unitaires.
Petr Peller
@ user11617 veuillez définir "API privée" et "API publique". À ma connaissance, l'API publique est l'API qui est visible pour les consommateurs externes / tiers et explicitement versionnée via SemVer ou similaire, tout le reste est privé. Avec cette définition, presque tous les tests unitaires testent des "API privées" et donc plus sensibles à la duplication de code, ce que je pense être vrai.
KolA
@KolA "Public" ne signifie pas des consommateurs tiers - ce n'est pas une API Web. L'API publique d'une classe fait référence aux méthodes qui sont censées être utilisées par le code client (qui généralement ne change pas / ne devrait pas changer beaucoup) - généralement les méthodes "publiques". L'API privée fait référence à la logique et aux méthodes utilisées en interne. Ceux-ci ne doivent pas être accessibles depuis l'extérieur de la classe. C'est l'une des raisons pour lesquelles il est important d'encapsuler correctement la logique dans une classe en utilisant des modificateurs d'accès ou la convention dans le langage utilisé.
Nathan
@Nathan tout package library / dll / nuget a des consommateurs tiers, il n'est pas nécessaire qu'il s'agisse d'une API Web. Ce que j'ai mentionné, c'est qu'il est très courant de déclarer des classes et des membres publics qui ne sont pas censés être utilisés directement par les consommateurs de la bibliothèque (ou au mieux les rendre internes et annoter l'assemblage avec InternalsVisibleToAttribute) juste pour permettre aux tests unitaires de les atteindre directement. Cela conduit à des tas de tests couplés à la mise en œuvre et en fait plus un fardeau qu'un avantage
KolA
186

La lisibilité est plus importante pour les tests. Si un test échoue, vous voulez que le problème soit évident. Le développeur ne devrait pas avoir à parcourir beaucoup de code de test fortement factorisé pour déterminer exactement ce qui a échoué. Vous ne voulez pas que votre code de test devienne si complexe que vous deviez écrire des tests de test unitaires.

Cependant, éliminer la duplication est généralement une bonne chose, tant que cela n'obscurcit rien, et éliminer la duplication dans vos tests peut conduire à une meilleure API. Assurez-vous simplement de ne pas dépasser le point des rendements décroissants.

Kristopher Johnson
la source
xUnit et d'autres contiennent un argument 'message' dans les appels d'assert. Bonne idée de mettre des phrases significatives pour permettre aux développeurs de trouver rapidement les résultats de test ayant échoué.
seand
1
@seand Vous pouvez essayer d'expliquer ce que votre assertion vérifie, mais quand elle échoue et qu'elle contient du code quelque peu obscurci, le développeur devra quand même aller le dérouler. IMO Il est plus important d'avoir du code pour s'y décrire.
IgorK
1
@Kristopher,? Pourquoi est-ce publié sur un wiki communautaire?
Pacerier
@Pacerier je ne sais pas. Il y avait des règles compliquées sur les choses qui devenaient automatiquement un wiki communautaire.
Kristopher Johnson
Pour la lisibilité des rapports est plus importante que les tests, en particulier lors de l'intégration ou des tests de bout en bout, les scénarios peuvent être suffisamment complexes pour éviter de naviguer un tout petit peu, il est correct de trouver l'échec mais encore une fois pour moi l'échec dans les rapports devrait expliquer le problème assez bien.
Anirudh le
47

Le code de mise en œuvre et les tests sont des animaux différents et les règles d'affacturage s'appliquent différemment à eux.

Le code ou la structure dupliquée est toujours une odeur dans le code d'implémentation. Lorsque vous commencez à avoir une mise en œuvre standard, vous devez réviser vos abstractions.

D'autre part, le code de test doit maintenir un niveau de duplication. La duplication dans le code de test atteint deux objectifs:

  • Garder les tests découplés. Un couplage de test excessif peut rendre difficile la modification d'un seul test défaillant qui doit être mis à jour car le contrat a changé.
  • Garder les tests significatifs isolément. Lorsqu'un seul test échoue, il doit être raisonnablement simple de savoir exactement ce qu'il teste.

J'ai tendance à ignorer la duplication triviale dans le code de test tant que chaque méthode de test reste inférieure à environ 20 lignes. J'aime quand le rythme setup-run-verify est apparent dans les méthodes de test.

Lorsque la duplication s'insinue dans la partie "vérifier" des tests, il est souvent avantageux de définir des méthodes d'assertion personnalisées. Bien entendu, ces méthodes doivent encore tester une relation clairement identifiée qui peut être mise en évidence dans le nom de la méthode: assertPegFitsInHole-> bon, assertPegIsGood-> mauvais.

Lorsque les méthodes de test deviennent longues et répétitives, je trouve parfois utile de définir des modèles de test à remplir les blancs qui prennent quelques paramètres. Ensuite, les méthodes de test réelles sont réduites à un appel à la méthode modèle avec les paramètres appropriés.

Quant à beaucoup de choses dans la programmation et les tests, il n'y a pas de réponse claire. Vous devez développer un goût et la meilleure façon de le faire est de faire des erreurs.

ddaa
la source
8

Je suis d'accord. Le compromis existe mais est différent selon les endroits.

Je suis plus susceptible de refactoriser le code dupliqué pour configurer l'état. Mais moins susceptible de refactoriser la partie du test qui exerce réellement le code. Cela dit, si l'exercice du code prend toujours plusieurs lignes de code, je pourrais penser que c'est une odeur et refactoriser le code réel sous test. Et cela améliorera la lisibilité et la maintenabilité du code et des tests.

stucampbell
la source
Je pense que c'est une bonne idée. Si vous avez beaucoup de duplication, voyez si vous pouvez refactoriser pour créer un "test fixture" commun sous lequel de nombreux tests peuvent s'exécuter. Cela éliminera le code de configuration / démontage en double.
Programmeur hors
8

Vous pouvez réduire la répétition en utilisant plusieurs types de méthodes de test .

Je suis plus tolérant à la répétition dans le code de test que dans le code de production, mais j'en ai parfois été frustré. Lorsque vous modifiez la conception d'une classe et que vous devez revenir en arrière et modifier 10 méthodes de test différentes qui font toutes les mêmes étapes de configuration, c'est frustrant.

Don Kirkby
la source
6

Jay Fields a inventé la phrase que «les DSL doivent être DAMP, pas DRY», où DAMP signifie des phrases descriptives et significatives . Je pense qu'il en va de même pour les tests. De toute évidence, trop de duplication est mauvaise. Mais éliminer à tout prix la duplication est encore pire. Les tests doivent servir de spécifications révélatrices d'intention. Si, par exemple, vous spécifiez la même fonction sous plusieurs angles différents, alors une certaine quantité de duplication est à prévoir.

Jörg W Mittag
la source
3

J'ADORE rspec à cause de ça:

Il a deux choses pour vous aider -

  • groupes d'exemples partagés pour tester un comportement commun.
    vous pouvez définir un ensemble de tests, puis «inclure» cet ensemble dans vos tests réels.

  • contextes imbriqués.
    vous pouvez essentiellement avoir une méthode 'setup' et 'teardown' pour un sous-ensemble spécifique de vos tests, pas seulement pour tous ceux de la classe.

Plus tôt les frameworks de test .NET / Java / autres adopteront ces méthodes, mieux ce sera (ou vous pouvez utiliser IronRuby ou JRuby pour écrire vos tests, ce que je pense personnellement est la meilleure option)

Orion Edwards
la source
3

Je pense que le code de test nécessite un niveau d'ingénierie similaire qui serait normalement appliqué au code de production. Il peut certainement y avoir des arguments en faveur de la lisibilité et je conviens que c'est important.

D'après mon expérience, cependant, je trouve que des tests bien pondérés sont plus faciles à lire et à comprendre. S'il y a 5 tests qui se ressemblent chacun à l'exception d'une variable qui a changé et de l'assertion à la fin, il peut être très difficile de trouver ce qu'est cet élément différent. De même, s'il est factorisé de sorte que seule la variable qui change est visible et l'assertion, alors il est facile de comprendre ce que fait le test immédiatement.

Trouver le bon niveau d'abstraction lors des tests peut être difficile et je pense que cela en vaut la peine.

Kevin Londres
la source
2

Je ne pense pas qu'il y ait une relation entre un code plus dupliqué et plus lisible. Je pense que votre code de test devrait être aussi bon que votre autre code. Le code non répétitif est plus lisible que le code dupliqué lorsqu'il est bien fait.

Paco
la source
2

Idéalement, les tests unitaires ne devraient pas beaucoup changer une fois qu'ils sont écrits, donc je pencherais vers la lisibilité.

Le fait d'avoir des tests unitaires aussi discrets que possible permet également de garder les tests concentrés sur les fonctionnalités spécifiques qu'ils ciblent.

Cela dit, j'ai tendance à essayer de réutiliser certains morceaux de code que je finis par utiliser encore et encore, comme le code de configuration qui est exactement le même dans un ensemble de tests.

17 sur 26
la source
2

"les a refactorisées pour les rendre plus DRY - l'intention de chaque test n'était plus claire"

Il semble que vous ayez eu du mal à effectuer la refactorisation. Je ne fais que deviner, mais si cela s'avère moins clair, cela ne signifie-t-il pas que vous avez encore plus de travail à faire pour avoir des tests raisonnablement élégants et parfaitement clairs?

C'est pourquoi les tests sont une sous-classe de UnitTest - vous pouvez donc concevoir de bonnes suites de tests qui sont correctes, faciles à valider et à effacer.

Dans les temps anciens, nous avions des outils de test qui utilisaient différents langages de programmation. Il était difficile (voire impossible) de concevoir des tests agréables, faciles à travailler.

Vous avez toute la puissance de - quel que soit le langage que vous utilisez - Python, Java, C # - alors utilisez bien ce langage. Vous pouvez obtenir un bon code de test, clair et pas trop redondant. Il n'y a pas de compromis.

S.Lott
la source