web-dev-qa-db-fra.com

Existe-t-il une telle chose que d'avoir trop de tests unitaires?

J'ai été chargé d'écrire des tests unitaires pour une application existante. Après avoir terminé mon premier fichier, j'ai 717 lignes de code de test pour 419 lignes de code d'origine.

Ce ratio va-t-il devenir ingérable alors que nous augmentons notre couverture de code?

Ma compréhension des tests unitaires consistait à tester chaque méthode de la classe pour m'assurer que chaque méthode fonctionnait comme prévu. Cependant, dans la demande de traction, mon responsable technique a indiqué que je devais me concentrer sur des tests de niveau supérieur. Il a suggéré de tester 4 à 5 cas d'utilisation les plus couramment utilisés avec la classe en question, plutôt que de tester de manière exhaustive chaque fonction.

Je fais confiance au commentaire de mon responsable technique. Il a plus d'expérience que moi, et il a un meilleur instinct en matière de conception de logiciels. Mais comment une équipe composée de plusieurs personnes écrit-elle des tests pour une norme aussi ambiguë? c'est-à-dire, comment puis-je connaître mes pairs et partager la même idée pour les "cas d'utilisation les plus courants"?

Pour moi, une couverture de tests unitaires à 100% est un objectif ambitieux, mais même si nous n'atteignions que 50%, nous saurions que 100% de ces 50% étaient couverts. Sinon, l'écriture de tests pour une partie de chaque fichier laisse beaucoup de place pour tricher.

143
user2954463

Oui, avec une couverture à 100%, vous écrirez des tests dont vous n'avez pas besoin. Malheureusement, le seul moyen fiable de déterminer les tests dont vous n'avez pas besoin est de les écrire tous, puis d'attendre environ 10 ans pour voir lesquels n'ont jamais échoué.

Maintenir un grand nombre de tests n'est généralement pas problématique. De nombreuses équipes ont automatisé l'intégration et les tests du système en plus d'une couverture de tests unitaires à 100%.

Cependant, vous n'êtes pas dans une phase de maintenance de test, vous jouez au rattrapage. Il est beaucoup mieux d'avoir 100% de vos cours à 50% de couverture de test que 50% de vos cours à 100% de couverture de test, et votre responsable semble essayer de vous faire allouer votre temps en conséquence. Une fois que vous avez cette référence, l'étape suivante consiste généralement à pousser à 100% les fichiers modifiés à l'avenir.

180
Karl Bielefeldt

Si vous avez travaillé sur de grandes bases de code créées à l'aide de Test Driven Development, vous savez déjà qu'il peut exister trop de tests unitaires. Dans certains cas, la majeure partie de l'effort de développement consiste à mettre à jour des tests de faible qualité qui seraient mieux mis en œuvre en tant que vérifications invariantes, préconditionnelles et postconditionnelles dans les classes pertinentes, au moment de l'exécution (c'est-à-dire les tests comme effet secondaire d'un test de niveau supérieur). ).

Un autre problème est la création de conceptions de mauvaise qualité, utilisant des techniques de conception axées sur le fret, qui entraînent une prolifération de choses à tester (plus de classes, d'interfaces, etc.). Dans ce cas, le fardeau peut sembler être la mise à jour du code de test, mais le vrai problème est une conception de mauvaise qualité.

68
Frank Hileman

Réponses à vos questions

Existe-t-il une telle chose que d'avoir trop de tests unitaires?

Bien sûr ... Vous pourriez, par exemple, avoir plusieurs tests qui semblent différents à première vue mais vraiment tester la même chose (dépendent logiquement des mêmes lignes de code d'application "intéressant" à tester).

Ou vous pouvez tester les éléments internes de votre code qui ne font jamais surface (c'est-à-dire qui ne font partie d'aucun type de contrat d'interface), où l'on pourrait se demander si cela a du sens, du tout. Par exemple, la formulation exacte des messages du journal interne ou autre.

J'ai été chargé d'écrire des tests unitaires pour une application existante. Après avoir terminé mon premier fichier, j'ai 717 lignes de code de test pour 419 lignes de code d'origine.

Cela me semble tout à fait normal. Vos tests dépensent beaucoup de lignes de code sur la configuration et le démontage en plus des tests réels. Le rapport peut s'améliorer ou non. Je suis moi-même assez lourd à tester, et investis souvent plus de temps et de temps sur les tests que le code réel.

Ce ratio va-t-il devenir ingérable alors que nous augmentons notre couverture de code?

Le rapport ne tient pas tellement compte. Il existe d'autres qualités de tests qui tendent à les rendre ingérables. Si vous devez régulièrement refactoriser tout un tas de tests lorsque vous effectuez des modifications assez simples dans votre code, vous devriez en examiner attentivement les raisons. Et ce ne sont pas le nombre de lignes que vous avez, mais la façon dont vous approchez le codage des tests.

Ma compréhension des tests unitaires consistait à tester chaque méthode de la classe pour m'assurer que chaque méthode fonctionnait comme prévu.

C'est correct pour les tests "unitaires" au sens strict. Ici, "unité" étant quelque chose comme une méthode ou une classe. Le but des tests "unitaires" est de ne tester qu'une seule unité de code spécifique, pas le système entier. Idéalement, vous supprimeriez tout le reste du système (en utilisant des doubles ou autres).

Cependant, dans la demande de traction, mon responsable technique a indiqué que je devais me concentrer sur des tests de niveau supérieur.

Ensuite, vous êtes tombé dans le piège de supposer que les gens voulaient dire des tests unitaires lorsqu'ils ont dit tests unitaires. J'ai rencontré de nombreux programmeurs qui disent "test unitaire" mais veulent dire quelque chose de très différent.

Il a suggéré de tester 4 à 5 cas d'utilisation les plus couramment utilisés avec la classe en question, plutôt que de tester de manière exhaustive chaque fonction.

Bien sûr, se concentrer uniquement sur les 80% de code importants réduit également la charge ... J'apprécie que vous ayez une haute estime de votre patron, mais cela ne me semble pas être le choix optimal.

Pour moi, une couverture de tests unitaires à 100% est un objectif ambitieux, mais même si nous n'atteignions que 50%, nous saurions que 100% de ces 50% étaient couverts.

Je ne sais pas ce qu'est la "couverture des tests unitaires". Je suppose que vous voulez dire "couverture de code", c'est-à-dire qu'après avoir exécuté la suite de tests, chaque ligne de code (= 100%) a été exécutée au moins une fois.

C'est une métrique de Nice, mais de loin pas la meilleure norme que l'on puisse viser. L'exécution de lignes de code n'est pas tout; cela ne tient pas compte des chemins différents à travers des branches compliquées et imbriquées, par exemple. C'est plutôt une métrique qui pointe du doigt des morceaux de code qui sont trop peu testés (évidemment, si une classe a une couverture de code de 10% ou 5%, alors quelque chose ne va pas); d'autre part, une couverture à 100% ne vous dira pas si vous avez suffisamment testé ou si vous avez testé correctement.

Test d'intégration

Cela m'énerve considérablement lorsque les gens parlent constamment de tests unitaires aujourd'hui, par défaut. À mon avis (et expérience), les tests unitaires sont parfaits pour les bibliothèques/API; dans les domaines plus commerciaux (où nous parlons de cas d'utilisation comme dans la question à l'étude), ils ne sont pas nécessairement la meilleure option.

Pour le code d'application général et dans l'entreprise moyenne (où gagner de l'argent, respecter les délais et satisfaire la satisfaction du client est important, et vous voulez principalement éviter les bogues qui sont soit directement dans le visage de l'utilisateur, soit qui pourraient conduire à catastrophes réelles - nous ne parlons pas des lancements de fusées de la NASA ici), les tests d'intégration ou de fonctionnalité sont beaucoup plus utile.

Ceux-ci vont de pair avec le développement piloté par le comportement ou le développement piloté par les fonctionnalités; ceux-ci ne fonctionnent pas par définition avec des tests unitaires (stricts).

Pour faire court (ish), un test d'intégration/de fonctionnalité exerce toute la pile des applications. Dans une application Web, cela agirait comme un navigateur qui clique sur l'application (et non, évidemment, il n'a pas pour être aussi simpliste, il y a sont des frameworks très puissants pour le faire - consultez http://cucumber.io pour un exemple).

Oh, pour répondre à vos dernières questions: vous obtenez une couverture de test élevée pour toute votre équipe en vous assurant qu'une nouvelle fonctionnalité n'est programmée qu'après la mise en œuvre et l'échec de son test de fonctionnalité. Et oui, cela signifie chaque fonctionnalité. Cela vous garantit une couverture de fonctionnalités à 100% (positive). Par définition, il garantit qu'une fonctionnalité de votre application ne "disparaîtra" jamais. Il ne garantit pas une couverture de code à 100% (par exemple, à moins que vous ne programmiez activement des fonctionnalités négatives, vous n'exercerez pas votre gestion des erreurs/gestion des exceptions).

Il ne vous garantit pas une application sans bug; bien sûr, vous voudrez écrire des tests de fonctionnalités pour des situations de buggy évidentes ou très dangereuses, une mauvaise entrée utilisateur, un piratage (par exemple, la gestion de session, la sécurité, etc.), etc .; mais même seulement la programmation des tests positifs a un énorme avantage et est tout à fait réalisable avec des frameworks modernes et puissants.

Les tests de fonctionnalités/d'intégration ont évidemment leur propre boîte de vers (par exemple, les performances; tests redondants de frameworks tiers; comme vous n'utilisez généralement pas de doublons, ils ont également tendance à être plus difficiles à écrire, d'après mon expérience ...), mais je '' d prendre une application testée à 100% de fonctionnalités positives par rapport à une application testée à 100% de couverture de code (pas une bibliothèque!) tous les jours.

36
AnoE

Oui, il est possible d'avoir trop de tests unitaires. Si vous avez une couverture à 100% avec des tests unitaires et aucun test d'intégration par exemple, vous avez un problème clair.

Quelques scénarios:

  1. Vous sur-concevez vos tests sur une implémentation spécifique. Ensuite, vous devez jeter les tests unitaires lorsque vous refactorisez, pour ne pas dire lorsque vous changez d'implémentation (un point de douleur très fréquent lors de l'optimisation des performances).

    Un bon équilibre entre les tests unitaires et les tests d'intégration réduit ce problème sans perdre de couverture significative.

  2. Vous pouvez avoir une couverture raisonnable pour chaque validation avec 20% des tests que vous avez, laissant les 80% restants pour l'intégration ou au moins des tests séparés; les effets négatifs majeurs que vous voyez dans ce scénario sont des changements lents car vous devez attendre longtemps pour que les tests s'exécutent.

  3. Vous modifiez trop le code pour vous permettre de le tester; par exemple, j'ai vu beaucoup d'abus d'IoC sur des composants qui ne nécessiteront jamais d'être modifiés ou au moins il est coûteux et peu prioritaire de les généraliser, mais les gens investissent beaucoup de temps à les généraliser et à les refactoriser pour permettre à l'unité de les tester. .

Je suis particulièrement d'accord avec la suggestion d'obtenir une couverture de 50% sur 100% des fichiers, au lieu d'une couverture de 100% sur 50% des fichiers; concentrez vos efforts initiaux sur les cas positifs les plus courants et les cas négatifs les plus dangereux, n'investissez pas trop dans la gestion des erreurs et les chemins inhabituels, non pas parce qu'ils ne sont pas importants mais parce que vous avez un temps limité et un univers de test infini, vous devez donc prioriser dans tous les cas.

25
Bruno Guardia

Gardez à l'esprit que chaque test a un coût et un avantage. Les inconvénients comprennent:

  • un test doit être écrit;
  • un test prend (généralement très peu de temps) pour s'exécuter;
  • un test doit être maintenu avec le code - les tests doivent changer lorsque les API qu'ils testent changent;
  • vous devrez peut-être modifier votre conception afin d'écrire un test (bien que ces changements soient généralement pour le mieux).

Si les coûts l'emportent sur les avantages, il vaut mieux ne pas rédiger un test. Par exemple, si la fonctionnalité est difficile à tester, l'API change souvent, l'exactitude est relativement peu importante et les chances que le test trouve un défaut est faible, vous feriez probablement mieux de ne pas l'écrire.

En ce qui concerne votre rapport particulier de tests au code, si le code est suffisamment dense en logique, ce rapport peut être garanti. Cependant, cela ne vaut probablement pas la peine de maintenir un ratio aussi élevé dans une application typique.

19

Oui, il y a trop de tests unitaires.

Bien que les tests soient bons, chaque test unitaire est:

  • Une charge de maintenance potentielle étroitement liée à l'API

  • Temps qui pourrait être consacré à autre chose

  • Une tranche de temps dans la suite de tests unitaires
  • Peut-être n'ajoute aucune valeur réelle car il s'agit en fait d'un doublon d'un autre test ayant une chance minuscule qu'un autre test passe et que ce test échoue.

Il est sage de viser une couverture de code à 100%, mais cela signifie loin d'être une suite de tests dont chacun fournit indépendamment une couverture de code à 100% sur un point d'entrée spécifié (fonction/méthode/appel, etc.).

Bien qu'il soit difficile d'obtenir une bonne couverture et d'éliminer les bogues, la vérité est probablement qu'il existe une telle chose comme "les mauvais tests unitaires" autant que "trop de tests unitaires".

La pragmatique pour la plupart des codes indique:

  1. Assurez-vous d'avoir une couverture de 100% des points d'entrée (tout est testé d'une manière ou d'une autre) et visez à être proche de la couverture de code à 100% des chemins "sans erreurs".

  2. Testez toutes les valeurs ou tailles min/max pertinentes

  3. Testez tout ce que vous pensez être un cas spécial amusant, en particulier des valeurs "étranges".

  4. Lorsque vous trouvez un bogue, ajoutez un test unitaire qui aurait révélé ce bogue et demandez-vous si des cas similaires devraient être ajoutés.

Pour des algorithmes plus complexes, considérez également:

  1. Faire des tests en masse de plusieurs cas.
  2. Comparaison du résultat à une implémentation de "force brute" et vérification des invariants.
  3. Utiliser une méthode pour produire des cas de test aléatoires et vérifier la force brute et les post-conditions, y compris les invariants.

Par exemple, vérifiez un algorithme de tri avec une entrée aléatoire et la validation des données est triée à la fin en les scannant.

Je dirais que votre responsable technique propose des tests de "cul nu minimal". J'offre des "tests de qualité de la plus haute valeur" et il y a un spectre entre les deux.

Peut-être que votre aîné sait que le composant que vous construisez sera intégré dans une pièce et une unité plus grandes testées plus en profondeur lorsqu'il sera intégré.

La leçon clé est d'ajouter des tests lorsque des bogues sont détectés. Ce qui m'amène à ma meilleure leçon sur le développement de tests unitaires:

Concentrez-vous sur les unités et non sur les sous-unités. Si vous construisez une unité à partir de sous-unités, écrivez des tests très basiques pour les sous-unités jusqu'à ce qu'elles soient plausibles et obtenez une meilleure couverture en testant les sous-unités via leurs unités de contrôle.

Donc, si vous écrivez un compilateur et que vous devez écrire une table de symboles (par exemple). Obtenez la table des symboles opérationnelle avec un test de base, puis travaillez (par exemple) l'analyseur de déclaration qui remplit la table. N'ajoutez d'autres tests à l'unité "autonome" de la table des symboles que si vous y trouvez des bogues. Sinon, augmentez la couverture par des tests unitaires sur l'analyseur de déclaration et plus tard sur l'ensemble du compilateur.

C'est le meilleur rapport qualité-prix (un test de l'ensemble teste plusieurs composants) et laisse plus de capacité pour une nouvelle conception et un raffinement car seule l'interface `` extérieure '' est utilisée dans les tests qui ont tendance à être plus stables.

Couplé avec les pré-conditions de test du code de débogage, les post-conditions, y compris les invariants à tous les niveaux, vous bénéficiez d'une couverture de test maximale grâce à une implémentation minimale des tests.

13
Persixty

Tout d'abord, ce n'est pas forcément un problème d'avoir plus lignes de test que de code de production. Le code de test est (ou devrait être) linéaire et facile à comprendre - sa complexité nécessaire est très, très faible, que le code de production le soit ou non. Si le complexité des tests commence à se rapprocher de celui du code de production, alors vous avez probablement un problème.

Oui, il est possible d'avoir trop de tests unitaires - une simple expérience de réflexion montre que vous pouvez continuer à ajouter des tests qui n'apportent pas de valeur supplémentaire, et que tous ces tests ajoutés peuvent inhiber au moins certains refactorings.

Le conseil de ne tester que les cas les plus courants est erroné, à mon avis. Ceux-ci peuvent agir comme des tests de fumée pour gagner du temps sur les tests du système, mais les tests vraiment précieux attrapent des cas difficiles à exercer dans tout le système. Par exemple, l'injection d'erreur contrôlée des échecs d'allocation de mémoire peut être utilisée pour exercer des chemins de récupération qui pourraient autrement être d'une qualité totalement inconnue. Ou passez zéro comme valeur dont vous savez qu'elle sera utilisée comme diviseur (ou un nombre négatif qui aura une racine carrée) et assurez-vous de ne pas obtenir d'exception non gérée.

Les tests suivants les plus précieux sont ceux qui exercent les limites extrêmes ou les points limites. Par exemple, une fonction qui accepte les mois (basés sur 1) de l'année doit être testée avec 0, 1, 12 et 13, afin que vous sachiez que les transitions valides-invalides sont au bon endroit. Il est surestimé d'utiliser également 2..11 pour ces tests.

Vous êtes dans une position difficile, car vous devez écrire des tests pour le code existant. Il est plus facile d'identifier les cas Edge lorsque vous écrivez (ou êtes sur le point d'écrire) le code.

3
Toby Speight

Ma compréhension des tests unitaires consistait à tester chaque méthode de la classe pour m'assurer que chaque méthode fonctionnait comme prévu.

Cette compréhension est fausse.

Les tests unitaires vérifient le comportement du nité sous test.

En ce sens, une nité n'est pas nécessairement "une méthode dans une classe". J'aime la définition d'une unité par Roy Osherove dans The Art of Unit Testing:

Une unité est tout le code de production qui a la même raison de changer.

Sur cette base, un test unitaire devrait vérifier chaque comportement souhaité de votre code. Où le "désir" est plus ou moins pris dans les exigences.


Cependant, dans la demande de traction, mon responsable technique a indiqué que je devais me concentrer sur des tests de niveau supérieur.

Il a raison, mais d'une manière différente de ce qu'il pense.

D'après votre question, je comprends que vous êtes le "testeur dédié" dans ce projet.

Le gros malentendu est qu'il s'attend à ce que vous écriviez des tests unitaires (contrairement à "tester en utilisant un cadre de tests unitaires"). Écrire des tests ynit est la responsabilité des développeurs, pas les testeurs (dans un monde idéal, je sais ...). D'un autre côté, vous avez tagué cette question avec TDD, ce qui implique exactement cela.

Votre travail en tant que testeur consiste à écrire (ou exécuter manuellement) des tests de module et/ou d'application. Et ce genre de tests devrait principalement vérifier que toutes les unités fonctionnent bien ensemble. Cela signifie que vous devez sélectionner vos cas de test afin que chaque unité soit exécutée au moins une fois. Et ce contrôle est que c'est s'exécute. Le résultat réel est moins important car il est susceptible de changer avec les exigences futures.

Pour souligner une fois de plus l'analogie de l'automobile de vidage: Combien de tests sont effectués avec une voiture à la fin de la chaîne de montage? Exactement un: il doit conduire jusqu'au parking tout seul ...

Le point ici est:

Nous devons être conscients de cette différence entre les "tests unitaires" et les "tests automatisés utilisant un cadre de tests unitaires".


Pour moi, une couverture de tests unitaires à 100% est un objectif ambitieux, mais même si nous n'atteignions que 50%, nous saurions que 100% de ces 50% étaient couverts.

Les tests unitaires sont un filet de sécurité. Ils vous donnent confiance en refactor votre code pour réduire la dette technique ou ajouter de nouveaux comportements sans craindre de casser les comportements déjà implémentés.

Vous n'avez pas besoin d'une couverture de code à 100%.

Mais vous avez besoin d'une couverture comportementale à 100%. (Oui, la couverture du code et la couverture du comportement sont en corrélation d'une manière ou d'une autre, mais elles ne sont pas identiques pour le plaisir.)

Si votre couverture de comportement est inférieure à 100%, une exécution réussie de votre suite de tests ne signifie rien, car vous auriez pu modifier certains comportements non testés. Et vous serez remarqué par votre client le lendemain de la mise en ligne de votre version ...


Conclusion

Peu de tests valent mieux qu'aucun test. Sans aucun doute!

Mais il n'y a rien de tel que d'avoir trop de tests unitaires.

En effet, chaque test unitaire vérifie une attente unique sur les codes comportement. Et vous ne pouvez pas écrire plus de tests unitaires que vous n'en attendez sur votre code. Et un trou dans votre harnais de sécurité est une chance pour un changement indésirable d'endommager le système de production.

3
Timothy Truckle

Absolument oui. J'étais un SDET pour une grande entreprise de logiciels. Notre petite équipe devait maintenir un code de test qui était auparavant géré par une équipe beaucoup plus grande. En plus de cela, notre produit avait certaines dépendances qui introduisaient constamment des changements de rupture, ce qui signifie une maintenance constante des tests pour nous. Nous n'avions pas la possibilité d'augmenter la taille de l'équipe, nous avons donc dû jeter des milliers de tests de moindre valeur en cas d'échec. Sinon, nous ne pourrions jamais suivre les défauts.

Avant de rejeter cela comme un simple problème de gestion, considérez que de nombreux projets dans le monde réel souffrent d'une réduction des effectifs à l'approche du statut hérité. Parfois, cela commence même à se produire juste après la première version.

2
mrog

Avoir plus de lignes de code de test que de code produit n'est pas nécessairement un problème, en supposant que vous refactorisez votre code de test pour éliminer le copier-coller.

Le problème est d'avoir des tests qui sont des miroirs de votre implémentation, sans signification commerciale - par exemple, des tests chargés de mocks et de stubs et affirmant seulement qu'une méthode appelle une autre méthode.

Une grande citation dans le "pourquoi la plupart des tests unitaires sont des déchets" papier est que les tests unitaires devraient avoir un "Oracle large, formel et indépendant d'exactitude, et ... une valeur commerciale attribuable"

1
wrschneider

Une chose que je n'ai pas vue mentionnée est que vos tests doivent être rapides et faciles pour que tout développeur puisse s'exécuter à tout moment.

Vous ne voulez pas avoir à vous connecter au contrôle de code source et attendre une heure ou plus (selon la taille de votre base de code) avant la fin des tests pour voir si votre modification a cassé quelque chose - vous voulez pouvoir le faire sur votre propre machine avant de vous connecter au contrôle de code source (ou au moins, avant de pousser vos modifications). Idéalement, vous devriez pouvoir exécuter vos tests avec un seul script ou bouton Push.

Et lorsque vous exécutez ces tests localement, vous voulez qu'ils s'exécutent rapidement - de l'ordre de quelques secondes. Plus lent, et vous serez tenté de ne pas les exécuter suffisamment ou pas du tout.

Donc, avoir autant de tests que les exécuter tous prend quelques minutes, ou avoir quelques tests trop complexes, pourrait être un problème.

0
mmathis