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 d'unité A, vous définissez (postulez) le comportement du simulacre de X sur X1. Le temps passe, les gens utilisent votre système, les besoins changent, X évolue: vous modifiez X pour afficher le comportement X2. De toute évidence, les tests unitaires pour X échoueront et vous devrez les adapter.
Mais quoi avec A? Les tests unitaires pour A n'échoueront pas lorsque le comportement de X sera modifié (en raison de la dérision de X). Comment détecter que le résultat de A sera différent s'il est exécuté avec le "réel" (modifié) X?
J'attends des réponses du type "Ce n'est pas le but des tests unitaires", mais quelle est alors la valeur des tests unitaires? Est-ce que cela vous dit vraiment seulement que lorsque tous les tests sont réussis, vous n'avez pas introduit de changement radical? Et lorsque le comportement de certaines classes 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?
la source
X1
vous dites queX
implémente l'interfaceX1
. Si vous modifiez l'interfaceX1
parX2
le modèle que vous avez utilisé dans les autres tests ne devrait plus être compilé, vous devez donc également corriger ces tests. Les changements dans le comportement de la classe ne devraient pas importer. En fait, votre classeA
ne devrait pas dépendre des détails de l’implémentation (ce que vous changeriez dans ce cas). Les tests unitairesA
sont donc toujours corrects et vous indiquent que celaA
fonctionne si l’implémentation de l’interface est idéale.Réponses:
Le faites vous? Je ne le fais pas, sauf obligation absolue. Je dois si:
X
est lent ouX
a des effets secondairesSi aucune de ces conditions ne s'applique, mes tests unitaires
A
testerontX
également. Faire autre chose, ce serait prendre des tests d'isolement à l'extrême illogique.Si vous avez des parties de votre code qui utilisent des exemples d'autres parties de votre code, je suis d'accord: quel est l'intérêt de tels tests unitaires? Alors ne fais pas ça. Laissez ces tests utiliser les dépendances réelles car ils forment ainsi des tests beaucoup plus précieux.
Et si des personnes s’énervent en appelant ces tests, des "tests unitaires", appelez-les simplement "tests automatisés" et continuez à écrire de bons tests automatisés.
la source
Vous avez besoin des deux. Des 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 connectés correctement. Le problème lié au fait de ne compter que sur les tests d'intégration est l'explosion combinatoire résultant d'interactions entre toutes vos unités.
Supposons que vous ayez la classe A, qui nécessite 10 tests unitaires pour couvrir 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, supposons que dans votre application, vous deviez alimenter la sortie de A en B. Votre code peut maintenant suivre 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 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 des tests d'intégration uniquement. Les tests intégrés de JB Rainsberger sont une arnaque HD
la source
Woah, attendez un moment. Les implications des tests pour l’échec de X sont trop importantes pour passer sous silence.
Si le changement d'implémentation de X de X1 à X2 interrompt les tests unitaires pour X, cela signifie que vous avez apporté une modification incompatible en arrière au contrat X.
X2 n’est pas un X, au sens de Liskov , vous devriez donc réfléchir à d’autres moyens de répondre aux besoins de vos parties prenantes (comme l’introduction d’une nouvelle spécification Y mise en œuvre par X2).
Pour des informations plus détaillées, voir Pieter Hinjens: La fin des versions de logiciels ou Rich Hickey Simple Made Easy .
Du point de vue de A, il est indispensable que le collaborateur respecte le contrat X. Et vous observez en fait que le test isolé pour A ne vous donne aucune assurance que A reconnaît les collaborateurs qui violent le contrat X.
Examiner 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 met en œuvre le contrat X correctement, et autant de tests isolés que nécessaire pour vous assurer que A fait ce qu'il convient en donnant des réponses intéressantes d'un X, et un nombre plus petit de tests intégrés pour s'assurer que X2 et A sont d'accord sur ce que signifie X.
Vous verrez parfois cette distinction exprimée en tests solitaires vs
sociable
tests; voir Jay Fields Travailler efficacement avec les tests unitaires .Encore une fois, 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 reposent sur des tests intégrés (orthographe de note). En résumé, sans les tests isolés / solitaires appliquant une pression sur la conception , la qualité se dégrade, ce qui entraîne davantage d'erreurs et des tests plus intégrés ....
Vous aurez également besoin de (quelques) tests d'intégration. Outre 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 des vérifications très rapides lorsque le travail est en cours, en enregistrant les vérifications supplémentaires lorsque vous pensez avoir terminé.
la source
Permettez-moi de commencer par dire que le principe de base de la question est imparfait.
Vous ne testez jamais (ni ne vous moquez) des implémentations, vous testez (et vous moquez) des 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 une maquette XM.
Supposons maintenant que nous changions X pour implémenter une nouvelle interface X2. Bien évidemment, mon code ne compile plus. A requiert 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 définie. Cependant, le simulateur XM n'implémente plus l'interface X1. Le problème a été identifié et peut être résolu.
La base entière du test unitaire et du mocking est que vous écrivez du code utilisant des interfaces. Le consommateur d'une interface ne se soucie pas de savoir comment le code est implémenté, seulement que le même contrat soit respecté (entrées / sorties).
Cela tombe en panne lorsque vos méthodes ont des effets secondaires, mais je pense que cela peut être exclu sans risque car "ne peut pas être testé ou simulé à l'unité".
la source
Répondant à vos questions:
Ils sont peu coûteux à écrire et à exécuter et vous obtenez un retour rapide. 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é toutes vos couches (oui, même dans la base de données).
Avoir des tests qui réussissent pourrait en dire très peu. Vous n'avez peut-être pas écrit assez de tests. Vous n'avez peut-être pas testé suffisamment de scénarios. La couverture de code peut aider ici mais ce n’est pas une solution miracle. Vous pouvez avoir des tests qui passent toujours . Le rouge est donc la première étape souvent négligée du rouge, du vert, du refactor.
Davantage de tests - bien que les outils deviennent de mieux en mieux. MAIS vous devriez définir le comportement de classe dans une interface (voir ci-dessous). NB: il y aura toujours une place pour les tests manuels au sommet de la pyramide de tests.
De plus en plus de tests d'intégration ne sont pas la solution non plus, ils sont coûteux à écrire, à exécuter et à maintenir. En fonction de votre configuration de build, votre responsable de build peut les exclure de toute façon, ce qui les rend dépendants du rappel du développeur (ce n'est jamais une bonne chose!).
J'ai vu des développeurs passer des heures à essayer de réparer les tests d'intégration brisés qu'ils auraient trouvés en cinq minutes s'ils disposaient de bons tests unitaires. Si vous ne le faites pas, essayez simplement d’exécuter le logiciel - c’est tout ce que vos utilisateurs finaux s’intéresseront. Inutile d'avoir des millions de tests unitaires réussis si tout le château de cartes s'effondre lorsque l'utilisateur exécute la suite complète.
Si vous voulez vous assurer que la classe A consomme la classe X de la même manière, vous devriez utiliser une interface plutôt qu'une concrétion. Ensuite, un changement radical est plus susceptible d’être repris au moment de la compilation.
la source
C'est correct.
Les tests unitaires sont là pour tester la fonctionnalité isolée d'une unité, une vérification à première vue que cela 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. Le test unitaire en soi n'est que légèrement meilleur que l'absence de test.
La raison pour laquelle nous avons des tests unitaires, c'est qu'ils sont censés être bon marché. Rapide à créer, exécuter et maintenir. Une fois que vous commencez à les transformer en tests d'intégration min, vous êtes dans un monde de douleur. Vous pouvez aussi bien faire un test d'intégration complet et ignorer les tests unitaires si vous voulez le faire.
maintenant, certaines personnes pensent qu'une unité n'est pas simplement une fonction dans une classe, mais toute la classe elle-même (moi-même inclus). Cependant, tout cela ne fait qu'augmenter la taille de l'unité. Vous aurez peut-être besoin de moins de tests d'intégration, mais vous en avez quand même besoin. Il est toujours impossible de vérifier que votre programme fait ce qu'il est supposé faire sans une suite de tests d'intégration complète.
et ensuite, vous devrez toujours exécuter les tests d'intégration complets sur un système actif (ou semi-actif) pour vérifier qu'il fonctionne avec les conditions utilisées par le client.
la source
Les tests unitaires ne prouvent pas l'exactitude de quoi que ce soit. Ceci est vrai pour tous les tests. Habituellement, les tests unitaires sont combinés à une conception basée sur contrat (conception par contrat est un autre moyen de le dire) et éventuellement à des épreuves automatisées de correction, si celle-ci doit être vérifiée régulièrement.
Si vous avez des contrats réels, comprenant des invariants de classe, des conditions préalables et des conditions de publication, il est possible de prouver l'exactitude hiérarchiquement, en basant l'exactitude 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.
la source
fac(5) == 120
, vous n'avez pas prouvé qu'ilfac()
rendait effectivement la factorielle de son argument. Vous avez seulement prouvé que vousfac()
restituez la factorielle de cinq à votre passage5
. Et même cela n’est pas certain, carfac()
il est concevable de revenir42
les premiers lundis après une éclipse totale à Tombouctou ... Le problème est qu’il est impossible de prouver la conformité en vérifiant les entrées de test individuelles, vous devez vérifier toutes les entrées possibles, et aussi prouver que vous n’avez rien oublié (comme lire l’horloge système).Je trouve les tests lourdement ridicules rarement utiles. La plupart du temps, je finis par réimplémenter le comportement que la classe d'origine a déjà, ce qui contrecarre totalement le but de se moquer.
Une meilleure stratégie consiste à bien séparer les problèmes (par exemple, vous pouvez tester la partie A de votre application sans importer les parties B à Z). Une telle bonne architecture aide vraiment à écrire un bon test.
De plus, je suis tout à fait disposé à accepter les effets secondaires tant que je peux les restaurer, par exemple, si ma méthode modifie les données de la base de données, laissez-les! Tant que je peux restaurer la base de données à son état précédent, quel est le problème? De plus, mon test permet de vérifier si les données sont conformes aux attentes. Les bases de données en mémoire ou les versions de test spécifiques de dbs sont vraiment utiles (par exemple, la version de test en mémoire de RavenDB).
Enfin, j'aime bien me moquer des limites du service, par exemple, ne faites pas cet appel http au service b, mais interceptons-le et introduisons une méthode appropriée.
la source
J'aimerais que les gens des deux camps comprennent que les tests de classe et de comportement ne sont pas orthogonaux.
Les tests de classe et les tests unitaires sont utilisés de manière interchangeable et ne devraient peut-être pas l'être. Certains tests unitaires viennent juste d’être implémentés dans des classes. C'est tout. Les tests unitaires sont pratiqués depuis des décennies dans des langues sans classes.
En ce qui concerne les comportements de test, il est parfaitement possible de le faire au sein de tests de classe en utilisant la construction GWT.
En outre, le fait que vos tests automatisés se déroulent en classe ou en comportement dépend plutôt de vos priorités. Certains auront besoin de prototyper rapidement et de sortir quelque chose tandis que d'autres auront des contraintes de couverture dues aux styles internes. De nombreuses raisons. Ce sont deux approches parfaitement valables. Vous payez votre argent, vous prenez votre choix.
Alors, que faire en cas de rupture de code? Si elle a été codée sur une interface, seule la concrétion doit changer (ainsi que tous les tests).
Cependant, l'introduction d'un nouveau comportement ne doit pas compromettre le système du tout. Linux et al sont pleins de fonctionnalités obsolètes. Et des choses telles que les constructeurs (et les méthodes) peuvent heureusement être surchargées sans obliger tout le code appelant à changer.
Lorsque les tests de classe gagnent, vous devez modifier une classe qui n'a pas encore été configurée (en raison de contraintes de temps, de complexité ou autre). Il est tellement plus facile de commencer avec une classe si elle comporte des tests complets.
la source
Sauf si l'interface pour X a changé, vous n'avez pas besoin de modifier le test unitaire pour A car rien de ce qui a été associé à A n'a été modifié. On dirait que vous avez vraiment écrit un test unitaire de X et A ensemble, mais que vous l'appeliez un test unitaire de A:
Idéalement, la maquette de X devrait simuler tous les comportements possibles de X, et pas seulement le comportement que vous attendez de X. Ainsi, peu importe ce que vous implémentez réellement dans X, A devrait déjà être capable de le gérer. Donc, aucune modification apportée à 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 soit un algorithme de tri et que A fournisse les données à trier. La maquette de X doit fournir une valeur de retour NULL, 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 en arrière, des listes avec le même élément répété, des valeurs nulles entremêlées, ridiculement grand nombre d'éléments, et il devrait également jeter 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, quand 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.
la source
Vous devez regarder différents tests.
Les tests unitaires eux-mêmes ne feront que tester X. Ils sont là pour vous empêcher de modifier le comportement de X mais ne pas sécuriser l'ensemble du système. Ils veillent à ce que vous puissiez refactoriser votre classe sans introduire de changement de comportement. Et si vous cassez X, vous le cassez ...
A devrait en effet simuler X pour ses tests unitaires et le test avec le simulateur devrait continuer à passer même après l'avoir modifié.
Mais il y a plus d'un niveau de test! Il existe également des tests d'intégration. Ces tests sont là pour valider l'interaction entre les classes. Ces tests ont généralement un prix plus élevé puisqu'ils n'utilisent pas de simulacres pour tout. Par exemple, un test d'intégration pourrait en réalité écrire un enregistrement dans une base de données, et un test unitaire ne devrait avoir aucune dépendance externe.
De plus, si X doit adopter un nouveau comportement, il serait préférable de fournir une nouvelle méthode permettant d'obtenir le résultat souhaité.
la source