web-dev-qa-db-fra.com

Comment détectez-vous les problèmes de dépendance avec les tests unitaires lorsque vous utilisez des objets fictifs?

Vous avez une classe X et vous écrivez des tests unitaires qui vérifient le comportement X1. Il y a aussi la classe A qui prend X comme dépendance.

Lorsque vous écrivez des tests unitaires pour A, vous vous moquez de X. En d'autres termes, lors du test unitaire de A, vous définissez (postulez) le comportement de la maquette de X sur X1. Le temps passe, les gens utilisent votre système, doivent changer, X évolue: vous modifiez X pour montrer le comportement X2. De toute évidence, les tests unitaires pour X échoueront et vous devrez les adapter.

Mais avec A? Les tests unitaires pour A n'échoueront pas lorsque le comportement de X est modifié (en raison de la moquerie de X). Comment détecter que le résultat de A sera différent lorsqu'il sera exécuté avec le X "réel" (modifié)?

J'attends des réponses du type: "Ce n'est pas le but des tests unitaires", mais quelle valeur ont alors les tests unitaires? Cela vous dit-il vraiment que lorsque tous les tests réussissent, vous n'avez pas introduit de changement de rupture? Et lorsque le comportement d'une classe change (volontairement ou non), comment pouvez-vous détecter (de préférence de manière automatisée) toutes les conséquences? Ne devrions-nous pas nous concentrer davantage sur les tests d'intégration?

98
bvgheluwe

Lorsque vous écrivez des tests unitaires pour A, vous vous moquez de X

Le faites vous? Je ne le fais pas, sauf si je dois absolument le faire. Je dois si:

  1. X est lent, ou
  2. X a des effets secondaires

Si aucun de ces éléments ne s'applique, mes tests unitaires de A testeront également X. Faire autre chose reviendrait à pousser les tests d'isolement à un extrême illogique.

Si vous avez des parties de votre code utilisant des simulations d'autres parties de votre code, alors je suis d'accord: quel est l'intérêt de ces tests unitaires? Alors ne fais pas ça. Laissez ces tests utiliser les dépendances réelles car ils forment des tests beaucoup plus précieux de cette façon.

Et si certaines personnes se fâchent lorsque vous appelez ces tests des "tests unitaires", appelez-les simplement "tests automatisés" et continuez à écrire de bons tests automatisés.

125
David Arno

Vous avez besoin des deux. Tests unitaires pour vérifier le comportement de chacune de vos unités, et quelques tests d'intégration pour vous assurer qu'ils sont correctement connectés. Le problème de se fier uniquement aux tests d'intégration est l'explosion combinatoire résultant des interactions entre toutes vos unités.

Disons que vous avez la classe A, qui nécessite 10 tests unitaires pour couvrir entièrement tous les chemins. Ensuite, vous avez une autre classe B, qui nécessite également 10 tests unitaires pour couvrir tous les chemins que le code peut emprunter. Maintenant, disons que dans votre application, vous devez alimenter la sortie de A en B. Maintenant, votre code peut prendre 100 chemins différents de l'entrée de A à la sortie de B.

Avec les tests unitaires, vous n'avez besoin que de 20 tests unitaires + 1 test d'intégration pour couvrir complètement tous les cas.

Avec les tests d'intégration, vous aurez besoin de 100 tests pour couvrir tous les chemins de code.

Voici une très bonne vidéo sur les inconvénients de s'appuyer uniquement sur les tests d'intégration les tests intégrés J B Rainsberger sont une arnaque HD

79
Eternal21

Lorsque vous écrivez des tests unitaires pour A, vous vous moquez de X. En d'autres termes, lors du test unitaire de A, vous définissez (postulez) le comportement de la maquette de X sur X1. Le temps passe, les gens utilisent votre système, doivent changer, X évolue: vous modifiez X pour montrer le comportement X2. De toute évidence, les tests unitaires pour X échoueront et vous devrez les adapter.

Woah, attends un instant. Les implications des tests pour l'échec de X sont trop importantes pour être passées sous silence.

Si le changement de l'implémentation de X de X1 à X2 rompt les tests unitaires de X, cela indique que vous avez apporté une modification incompatible vers l'arrière au contrat X.

X2 n'est pas un X, dans le sens Liskov , donc vous devriez penser à d'autres façons de répondre aux besoins de vos parties prenantes (comme l'introduction d'une nouvelle spécification Y, qui est implémentée par X2).

Pour des informations plus approfondies, voir Pieter Hinjens: The End of Software Versions , ou Rich Hickey Simple Made Easy .

Du point de vue de A, il y a précondition que le collaborateur respecte le contrat X. Et votre observation est effectivement que le test isolé pour A ne vous donne aucune assurance que A reconnaît les collaborateurs qui violent le Contrat X.

Examen les tests intégrés sont une arnaque ; en haut niveau, vous devez avoir autant de tests isolés que nécessaire pour vous assurer que X2 implémente correctement le contrat X, et autant de tests isolés que nécessaire pour vous assurer que A fait la bonne chose, compte tenu des réponses intéressantes d'un X, et un plus petit nombre de tests intégrés pour s'assurer que X2 et A s'accordent sur ce que signifie X.

Vous verrez parfois cette distinction exprimée comme solitaire tests vs sociable tests; voir Jay Fields Travailler efficacement avec les tests unitaires .

Ne devrions-nous pas nous concentrer davantage sur les tests d'intégration?

Encore une fois, voir les tests intégrés sont une arnaque - Rainsberger décrit en détail une boucle de rétroaction positive qui est commune (dans ses expériences) aux projets qui s'appuient sur des tests intégrés (orthographe de note). En résumé, sans que les tests isolés/solitaires exercent une pression sur la conception , la qualité se dégrade, conduisant à plus d'erreurs et à des tests plus intégrés ....

Vous aurez également besoin de (certains) tests d'intégration. En plus de la complexité introduite par plusieurs modules, l'exécution de ces tests a tendance à avoir plus de traînée que les tests isolés; il est plus efficace d'itérer sur des vérifications très rapides lorsque le travail est en cours, en sauvegardant les vérifications supplémentaires lorsque vous pensez avoir "terminé".

73
VoiceOfUnreason

Permettez-moi de commencer par dire que la prémisse centrale de la question est erronée.

Vous ne testez jamais (ou ne vous moquez pas) des implémentations, vous testez (et vous moquez) interfaces.

Si j'ai une vraie classe X qui implémente l'interface X1, je peux écrire une maquette XM qui est également conforme à X1. Ensuite, ma classe A doit utiliser quelque chose qui implémente X1, qui peut être soit la classe X soit la simulation XM.

Supposons maintenant que nous modifions X pour implémenter une nouvelle interface X2. Eh bien, évidemment, mon code ne compile plus. A nécessite quelque chose qui implémente X1 et qui n'existe plus. Le problème a été identifié et peut être résolu.

Supposons qu'au lieu de remplacer X1, nous le modifions simplement. Maintenant, la classe A est prête. Cependant, la maquette XM n'implémente plus l'interface X1. Le problème a été identifié et peut être résolu.


La base du test unitaire et de la simulation est que vous écrivez du code qui utilise des interfaces. Un consommateur d'une interface ne se soucie pas comment le code est implémenté, seulement que le même contrat est respecté (entrées/sorties).

Cela tombe en panne lorsque vos méthodes ont des effets secondaires, mais je pense que cela peut être exclu en toute sécurité car "ne peut pas être testé ou moqué".

15
Vlad274

Prendre vos questions tour à tour:

quelle valeur ont alors les tests unitaires

Ils ne coûtent pas cher à écrire et à exécuter et vous obtenez des commentaires précoces. Si vous cassez X, vous découvrirez plus ou moins immédiatement si vous avez de bons tests. N'envisagez même pas d'écrire des tests d'intégration à moins que vous n'ayez testé en bloc toutes vos couches (oui, même sur la base de données).

Est-ce que cela vous dit vraiment que lorsque tous les tests réussissent, vous n'avez pas introduit de changement de rupture

Avoir des tests réussis pourrait en dire très peu. Vous n'avez peut-être pas écrit suffisamment de tests. Vous n'avez peut-être pas testé suffisamment de scénarios. La couverture du code peut aider ici, mais ce n'est pas une solution miracle. Vous pouvez avoir des tests qui toujours passent. Par conséquent, le rouge est la première étape souvent négligée du refactoriseur rouge, vert.

Et lorsque le comportement d'une classe change (volontairement ou non), comment pouvez-vous détecter (de préférence de manière automatisée) toutes les conséquences

Plus de tests - bien que les outils s'améliorent de plus en plus. MAIS vous devez définir le comportement des classes dans une interface (voir ci-dessous). N.B. il y aura toujours une place pour les tests manuels au sommet de la pyramide des tests.

Ne devrions-nous pas nous concentrer davantage sur les tests d'intégration?

De plus en plus de tests d'intégration ne sont pas la réponse non plus, ils sont coûteux à écrire, à exécuter et à maintenir. En fonction de la configuration de votre build, votre gestionnaire de build peut les exclure de toute façon, ce qui les rend dépendants du souvenir d'un développeur (jamais une bonne chose!).

J'ai vu des développeurs passer des heures à essayer de réparer des tests d'intégration défectueux qu'ils auraient trouvés en cinq minutes s'ils avaient de bons tests unitaires. À défaut, essayez simplement d'exécuter le logiciel - c'est tout ce dont vos utilisateurs finaux se soucieront. Inutile d'avoir des millions de tests unitaires qui réussissent si tout le château de cartes tombe en panne lorsque l'utilisateur exécute toute la suite.

Si vous voulez vous assurer que la classe A consomme la classe X de la même manière, vous devez utiliser une interface plutôt qu'une concrétion. Ensuite, un changement de rupture est plus susceptible d'être détecté au moment de la compilation.

9
Robbie Dee

C'est correct.

Les tests unitaires sont là pour tester la fonctionnalité isolée d'une unité, un contrôle à première vue qu'elle fonctionne comme prévu et ne contient pas d'erreurs stupides.

Les tests unitaires ne sont pas là pour vérifier que l'application entière fonctionne.

Ce que beaucoup de gens oublient, c'est que les tests unitaires ne sont que le moyen le plus rapide et le plus sale de valider votre code. Une fois que vous savez que vos petites routines fonctionnent, vous devez également exécuter des tests d'intégration. Les tests unitaires en eux-mêmes ne sont que légèrement meilleurs que l'absence de tests.

La raison pour laquelle nous avons des tests unitaires est qu'ils sont censés être bon marché. Rapide à créer, à exécuter et à entretenir. Une fois que vous commencez à les transformer en tests d'intégration min, vous êtes dans un monde de douleur. Vous pourriez aussi bien faire un test d'intégration complet et ignorer complètement les tests unitaires si vous voulez le faire.

maintenant, certaines personnes pensent qu'une unité n'est pas seulement une fonction dans une classe, mais toute la classe elle-même (moi y compris). Cependant, tout cela ne fait qu'augmenter la taille de l'unité de sorte que vous pourriez avoir besoin de moins de tests d'intégration, mais vous en avez toujours besoin. Il est toujours impossible de vérifier que votre programme fait ce qu'il est censé faire sans une suite de tests d'intégration complète.

puis, vous devrez toujours exécuter les tests d'intégration complets sur un système en direct (ou semi-direct) pour vérifier qu'il fonctionne avec les conditions utilisées par le client.

9
gbjbaanb

Les tests unitaires ne prouvent pas l'exactitude de quoi que ce soit. Cela est vrai pour tous les tests. Habituellement, les tests unitaires sont combinés avec une conception contractuelle (la conception par contrat est une autre façon de le dire) et éventuellement des preuves d'exactitude automatisées, si l'exactitude doit être vérifiée régulièrement.

Si vous avez de vrais contrats, comprenant des invariants de classe, des conditions préalables et des conditions de publication, il est possible de prouver la justesse de manière hiérarchique, en basant la justesse des composants de niveau supérieur sur les contrats des composants de niveau inférieur. C'est le concept fondamental de la conception par contrat.

2
Frank Hileman

Je trouve que les tests fortement moqués sont rarement utiles. La plupart du temps, je finis par réimplémenter un comportement que la classe d'origine a déjà, ce qui va totalement à l'encontre du but de la moquerie.

Moi. une meilleure stratégie consiste à bien séparer les préoccupations (par exemple, vous pouvez tester la partie A de votre application sans introduire les parties B à Z). Une si bonne architecture aide vraiment à écrire un bon test.

De plus, je suis plus que disposé à accepter les effets secondaires aussi longtemps que je peux les annuler, par exemple si ma méthode modifie les données dans la base de données, laissez-le! Tant que je peux restaurer la base de données à l'état précédent, quel est le mal? De plus, il y a l'avantage que mon test peut vérifier si les données semblent comme prévu. Les bases de données en mémoire ou les versions de test spécifiques de dbs aident vraiment ici (par exemple, la version de test en mémoire de RavenDB).

Enfin, j'aime me moquer des limites des services, par exemple ne faites pas cet appel http au service b, mais interceptons-le et introduisons une personne appropriée

2
Christian Sauer

J'aimerais que les gens des deux camps comprennent que les tests de classe et les tests de comportement ne sont pas orthogonaux.

Les tests de classe et les tests unitaires sont interchangeables et ils ne devraient peut-être pas l'être. Il se trouve que certains tests unitaires sont implémentés dans les classes. C'est tout. Les tests unitaires ont eu lieu pendant des décennies dans des langues sans classes.

En ce qui concerne les tests de comportements, il est parfaitement possible de le faire dans les tests de classe en utilisant la construction GWT.

De plus, le fait que vos tests automatisés se déroulent selon des classes ou des comportements dépend plutôt de vos priorités. Certains auront besoin de prototyper rapidement et de sortir quelque chose par la porte tandis que d'autres auront des contraintes de couverture en raison des styles internes. De nombreuses raisons. Ce sont deux approches parfaitement valables. Vous payez votre argent, vous faites votre choix.

Alors, que faire en cas de rupture de code. S'il a été codé sur une interface, alors juste la concrétion doit changer (avec tous les tests).

Cependant, l'introduction d'un nouveau comportement ne doit pas du tout compromettre le système. Linux et al regorgent de fonctionnalités obsolètes. Et des choses comme les constructeurs (et les méthodes) peuvent heureusement être surchargées sans forcer tout le code appelant à changer.

Lorsque les tests de classe gagnent, c'est là que vous devez apporter une modification à une classe qui n'a pas encore été connectée (en raison de contraintes de temps, de complexité ou autre). Il est tellement plus facile de commencer avec une classe si elle a des tests complets.

1
Belgian Dog

À moins que l'interface de X ne change, vous n'avez pas besoin de changer le test unitaire pour A car rien lié à A n'a changé. Il semble que vous ayez vraiment écrit un test unitaire de X et A ensemble, mais que vous l'appeliez un test unitaire de A:

Lorsque vous écrivez des tests unitaires pour A, vous vous moquez de X. En d'autres termes, lors du test unitaire de A, vous définissez (postulez) le comportement de la maquette de X sur X1.

Idéalement, la maquette de X devrait simuler tout possible les comportements de X, pas seulement le comportement que vous attendez de X. Donc, peu importe ce que vous implémentez réellement dans X, A devrait déjà être capable de le gérer. Ainsi, aucun changement vers X, autre que la modification de l'interface elle-même, n'aura d'effet sur le test unitaire pour A.

Par exemple: Supposons que A est un algorithme de tri et A fournit des données à trier. La maquette de X devrait fournir une valeur de retour nulle, une liste de données vide, un seul élément, plusieurs éléments déjà triés, plusieurs éléments non déjà triés, plusieurs éléments triés à l'envers, des listes avec le même élément répété, des valeurs nulles entremêlées, un ridiculement grand nombre d'éléments, et il devrait également lever une exception.

Alors peut-être que X a initialement renvoyé des données triées le lundi et des listes vides le mardi. Mais maintenant, lorsque X renvoie des données non triées le lundi et lève des exceptions le mardi, A s'en fiche - ces scénarios étaient déjà couverts dans le test unitaire de A.

0
Moby Disk