web-dev-qa-db-fra.com

(Pourquoi) est-il important qu'un test unitaire ne teste pas les dépendances?

Je comprends la valeur des tests automatisés et je les utilise partout où le problème est suffisamment bien spécifié pour que je puisse trouver de bons cas de test. J'ai remarqué, cependant, que certaines personnes ici et sur StackOverflow mettent l'accent sur le test niquement une unité, pas ses dépendances. Ici, je ne vois pas l'avantage.

Le mocking/stubbing pour éviter de tester les dépendances ajoute de la complexité aux tests. Il ajoute des exigences de flexibilité/découplage artificielles à votre code de production pour prendre en charge la simulation. (Je suis en désaccord avec tous ceux qui disent que cela favorise une bonne conception. Écrire du code supplémentaire, introduire des éléments comme les cadres d'injection de dépendances, ou ajouter de la complexité à votre base de code pour rendre les choses plus flexibles/enfichables/extensibles/découplées sans un cas d'utilisation réel est une ingénierie excessive, pas bon design.)

Deuxièmement, tester les dépendances signifie que le code de bas niveau critique utilisé partout est testé avec des entrées autres que celles auxquelles celui qui a écrit ses tests a explicitement pensé. J'ai trouvé beaucoup de bogues dans les fonctionnalités de bas niveau en exécutant des tests unitaires sur les fonctionnalités de haut niveau sans se moquer des fonctionnalités de bas niveau dont elles dépendaient. Idéalement, ceux-ci auraient été trouvés par les tests unitaires pour la fonctionnalité de bas niveau, mais des cas manqués se produisent toujours.

Quel est l'autre côté de cela? Est-il vraiment important qu'un test unitaire ne teste pas également ses dépendances? Si oui, pourquoi?

Edit: je peux comprendre la valeur de se moquer des dépendances externes comme les bases de données, les réseaux, les services Web, etc. (Merci à Anna Lear de m'avoir motivée à clarifier ce point. .) Je faisais référence aux dépendances internes , c'est-à-dire aux autres classes, fonctions statiques, etc. qui n'ont pas de dépendances externes directes.

107
dsimcha

C'est une question de définition. Un test avec des dépendances est un test d'intégration, pas un test unitaire. Vous devriez également avoir une suite de tests d'intégration. La différence est que la suite de tests d'intégration peut être exécutée dans un cadre de test différent et probablement pas dans le cadre de la génération car elle prend plus de temps.

Pour notre produit: Nos tests unitaires sont exécutés avec chaque build, en quelques secondes. Un sous-ensemble de nos tests d'intégration s'exécute à chaque enregistrement, ce qui prend 10 minutes. Notre suite d'intégration complète est exécutée chaque nuit, en 4 heures.

118
Jeffrey Faust

Tester avec toutes les dépendances en place est toujours important, mais c'est plus dans le domaine des tests d'intégration comme l'a dit Jeffrey Faust.

L'un des aspects les plus importants des tests unitaires est de rendre vos tests dignes de confiance. Si vous ne croyez pas qu'un test réussi signifie vraiment que les choses vont bien et qu'un test échoué signifie vraiment un problème dans le code de production, vos tests ne sont pas aussi utiles qu'ils peuvent l'être.

Afin de rendre vos tests dignes de confiance, vous devez faire quelques choses, mais je vais me concentrer sur un seul pour cette réponse. Vous devez vous assurer qu'ils sont faciles à exécuter, afin que tous les développeurs puissent les exécuter facilement avant de vérifier le code. "Facile à exécuter" signifie que vos tests s'exécutent rapidement et il n'y a pas de configuration étendue ou configuration nécessaire pour les faire partir. Idéalement, tout le monde devrait pouvoir consulter la dernière version du code, exécuter les tests immédiatement et les voir passer.

Le fait d'abstraire les dépendances sur d'autres choses (système de fichiers, base de données, services Web, etc.) vous permet d'éviter d'exiger une configuration et rend vous et les autres développeurs moins sensibles aux situations où vous êtes tenté de dire "Oh, les tests ont échoué parce que je ne ' t avoir le partage réseau configuré. Oh bien. Je les exécuterai plus tard. "

Si vous voulez tester ce que vous faites avec certaines données, vos tests unitaires pour ce code logique métier ne devraient pas se soucier de comment vous obtenez ces données. Être capable de tester la logique de base de votre application sans dépendre de supports tels que les bases de données est génial. Si vous ne le faites pas, vous passez à côté.

P.S. Je dois ajouter qu'il est certainement possible de sur-concevoir au nom de la testabilité. Tester votre application permet d'atténuer cela. Mais en tout état de cause, une mauvaise implémentation d'une approche ne rend pas l'approche moins valide. Tout peut être utilisé à mauvais escient et exagéré si l'on ne continue pas à demander "pourquoi je fais ça?" tout en se développant.


public class MyClass 
{
    private SomeClass someClass;
    public MyClass()
    {
        someClass = new SomeClass();
    }

    // use someClass in some way
}

Je ne me soucie généralement pas de la façon dont SomeClass est créé. Je veux juste l'utiliser. Si SomeClass change et nécessite maintenant des paramètres au constructeur ... ce n'est pas mon problème. Je ne devrais pas avoir à changer MyClass pour s'adapter à cela.

Maintenant, cela touche simplement la partie design. En ce qui concerne les tests unitaires, je veux également me protéger des autres cours. Si je teste MyClass, j'aime savoir qu'il n'y a pas de dépendances externes, que SomeClass n'a pas à un moment donné introduit une connexion à la base de données ou un autre lien externe.

Mais un problème encore plus important est que je sais également que certains résultats de mes méthodes reposent sur la sortie d'une méthode sur SomeClass. Sans moquer/écraser SomeClass, je n'aurais peut-être aucun moyen de varier cette entrée en demande. Si j'ai de la chance, je pourrais peut-être composer mon environnement à l'intérieur du test de manière à ce qu'il déclenche la bonne réponse de SomeClass, mais en procédant ainsi, cela introduit de la complexité dans mes tests et les rend fragiles.

Réécrire MyClass pour accepter une instance de SomeClass dans le constructeur me permet de créer une fausse instance de SomeClass qui retourne la valeur que je veux (soit via un framework de simulation, soit avec une simulation manuelle). Je n'ai généralement pas ai pour introduire une interface dans ce cas. Que ce soit le cas ou non est à bien des égards un choix personnel qui peut être dicté par le langage de votre choix (par exemple, les interfaces sont plus susceptibles en C #, mais vous n'en aurez certainement pas besoin en Ruby).

39
Adam Lear

Mis à part le problème de test unitaire vs d'intégration, tenez compte des points suivants.

Class Widget a des dépendances sur les classes Thingamajig et WhatsIt.

Un test unitaire pour Widget échoue.

Dans quelle classe se situe le problème?

Si vous avez répondu "déclencher le débogueur" ou "lire le code jusqu'à ce que je le trouve", vous comprenez l'importance de tester uniquement l'unité, pas les dépendances.

22
Brook

Imaginez que la programmation est comme la cuisine. Ensuite, les tests unitaires reviennent à s'assurer que vos ingrédients sont frais, savoureux, etc. Alors que les tests d'intégration, c'est comme s'assurer que votre repas est délicieux.

En fin de compte, s'assurer que votre repas est délicieux (ou que votre système fonctionne) est la chose la plus importante, le but ultime. Mais si vos ingrédients, je veux dire, les unités fonctionnent, vous aurez un moyen moins cher de le découvrir.

En effet, si vous pouvez garantir le fonctionnement de vos unités/méthodes, vous avez plus de chances d'avoir un système fonctionnel. J'insiste sur "plus probable", par opposition à "certain". Vous avez toujours besoin de vos tests d'intégration, tout comme vous avez toujours besoin de quelqu'un pour goûter le repas que vous avez préparé et vous dire que le produit final est bon. Vous aurez plus de facilité à vous y rendre avec des ingrédients frais, c'est tout.

15
Julio

Tester les unités en les isolant complètement permettra de tester TOUTES les variations possibles des données et des situations dans lesquelles cette unité peut être soumise. Parce qu'il est isolé du reste, vous pouvez ignorer les variations qui n'ont pas d'effet direct sur l'unité à tester. à son tour, cela réduira considérablement la complexité des tests.

Les tests avec différents niveaux de dépendances intégrés vous permettront de tester des scénarios spécifiques qui pourraient ne pas avoir été testés dans les tests unitaires.

Les deux sont importants. Le simple fait de faire des tests unitaires entraînera invariablement des erreurs subtiles plus complexes lors de l'intégration des composants. Le simple fait de tester l'intégration signifie que vous testez un système sans être sûr que les différentes parties de la machine n'ont pas été testées. Penser que vous pouvez obtenir un meilleur test complet simplement en faisant des tests d'intégration est presque impossible car plus vous ajoutez de composants, plus le nombre de combinaisons d'entrées augmente TRÈS rapidement (pensez factoriel) et créer un test avec une couverture suffisante devient très rapidement impossible.

Donc, en bref, les trois niveaux de "tests unitaires" que j'utilise généralement dans presque tous mes projets:

  • Tests unitaires, isolés avec des dépendances moquées ou tronquées pour tester l'enfer d'un seul composant. Idéalement, on tenterait une couverture complète.
  • Tests d'intégration pour tester les erreurs les plus subtiles. Des vérifications ponctuelles soigneusement conçues qui montreraient les cas limites et les conditions typiques. Une couverture complète est souvent impossible, les efforts se concentrant sur ce qui ferait échouer le système.
  • Test de spécification pour injecter diverses données réelles dans le système, instrumenter les données (si possible) et observer la sortie pour s'assurer qu'elle est conforme aux spécifications et aux règles métier.

L'injection de dépendance est un moyen très efficace pour y parvenir car elle permet d'isoler très facilement les composants pour les tests unitaires sans ajouter de complexité au système. Votre scénario d'entreprise peut ne pas justifier l'utilisation du mécanisme d'injection, mais votre scénario de test le sera presque. Cela me suffit pour rendre alors indispensable. Vous pouvez également les utiliser pour tester les différents niveaux d'abstraction indépendamment via des tests d'intégration partielle.

6
Newtopian

Quel est l'autre côté de cela? Est-il vraiment important qu'un test unitaire ne teste pas également ses dépendances? Si oui, pourquoi?

Unité. Signifie singulier.

Tester 2 choses signifie que vous avez les deux choses et toutes les dépendances fonctionnelles.

Si vous ajoutez une troisième chose, vous augmentez les dépendances fonctionnelles dans les tests au-delà de manière linéaire. Les interconnexions entre les choses croissent plus rapidement que le nombre de choses.

Il existe n (n-1)/2 dépendances potentielles parmi n éléments testés.

Voilà la grande raison.

La simplicité a de la valeur.

4
S.Lott

Rappelez-vous comment vous avez appris la récursivité pour la première fois? Mon prof a dit "Supposons que vous ayez une méthode qui fait x" (par exemple résout fibbonacci pour tout x). "Pour résoudre pour x, vous devez appeler cette méthode pour x-1 et x-2". De la même manière, la suppression des dépendances vous permet de faire semblant qu'elles existent et de tester que l'unité actuelle fait ce qu'elle devrait faire. L'hypothèse est bien sûr que vous testez les dépendances aussi rigoureusement.

Il s'agit essentiellement du SRP à l'œuvre. Se concentrer sur une seule responsabilité, même pour vos tests, supprime la quantité de jonglage mental que vous devez faire.

2
Michael Brown

(Ceci est une réponse mineure. Merci @TimWilliscroft pour l'astuce.)

Les défauts sont plus faciles à localiser si:

  • L'unité sous test et chacune de ses dépendances sont testées indépendamment.
  • Chaque test échoue si et seulement s'il y a un défaut dans le code couvert par le test.

Cela fonctionne bien sur papier. Cependant, comme illustré dans la description de OP (les dépendances sont boguées), si les dépendances ne sont pas testées, il serait difficile de localiser l'emplacement du défaut.

2
rwong

Beaucoup de bonnes réponses. J'ajouterais également quelques autres points:

Les tests unitaires vous permettent également de tester votre code lorsque vos dépendances n'existent pas . Par exemple. vous ou votre équipe n'avez pas encore écrit les autres couches, ou vous attendez peut-être une interface fournie par une autre entreprise.

Les tests unitaires signifient également que vous n'avez pas besoin d'avoir un environnement complet sur votre machine de développement (par exemple une base de données, un serveur Web, etc.). Je recommande fortement à tous les développeurs do d'avoir un tel environnement, quelle que soit sa taille, afin de reproduire les bogues, etc. Cependant, si pour une raison quelconque, il n'est pas possible d'imiter l'environnement de production, alors l'unité tester au moins vous donne un certain niveau de confiance dans votre code avant qu'il ne passe à un système de test plus grand.

2
Steve

Euh ... il y a de bons points sur les tests unitaires et d'intégration écrits dans ces réponses ici!

Je manque les vues liées aux coûts et pratiques ici. Cela dit, je vois clairement l'avantage des tests unitaires très isolés/atomiques (peut-être très indépendants les uns des autres et avec la possibilité de les exécuter en parallèle et sans aucune dépendance, comme les bases de données, le système de fichiers, etc.) et les tests d'intégration (de niveau supérieur), mais ... c'est aussi une question de coûts (temps, argent, ...) et de risques.

Il y a donc d'autres facteurs, qui sont beaucoup plus importants (comme "que tester" ) avant de penser à "comment tester" d'après mon expérience ...

Mon client paie-t-il (implicitement) le supplément d'écriture et de maintenance des tests? Une approche plus axée sur les tests (écrivez-vous les tests avant d'écrire le code) est-elle vraiment rentable dans mon environnement (risque de défaillance du code/analyse des coûts, personnes, spécifications de conception, configuration de l'environnement de test)? Le code est toujours bogué, mais pourrait-il être plus rentable de déplacer les tests vers l'utilisation en production (dans le pire des cas!)?

Cela dépend aussi beaucoup de la qualité de votre code (normes) ou des cadres, de l'IDE, des principes de conception, etc. que vous et votre équipe suivez et de leur expérience. Un document bien écrit, facilement compréhensible et suffisamment documenté ( idéalement auto-documenté), modulaire, ... le code introduit probablement moins de bogues que l'inverse. Ainsi, le "besoin" réel, la pression ou les coûts/risques globaux de maintenance/correction de bogues pour les tests approfondis peuvent ne pas être élevés.

Prenons-le à l'extrême où un collègue de mon équipe a suggéré, nous devons essayer de tester notre code de couche de modèle Java EE Java pur avec une couverture de 100% souhaitée pour toutes les classes au sein et ou le gestionnaire qui souhaite que les tests d'intégration soient couverts avec 100% de tous les cas d'utilisation possibles dans le monde réel et les flux de travail de l'interface utilisateur Web, car nous ne voulons pas risquer l'échec de tout cas d'utilisation. Mais nous avons un budget serré d'environ 1 million d'euros, un plan assez serré pour tout coder. Un environnement client, où les bugs potentiels de l'application ne seraient pas un très grand danger pour les humains ou les entreprises. Notre application sera testée en interne avec (quelques) tests unitaires importants, tests d'intégration , des tests clients clés avec des plans de test conçus, une phase de test, etc. Nous ne développons pas d'application pour certaines usines nucléaires ou la production pharmaceutique! (Nous avons une seule base de données désignée, un clone facile à tester pour chaque développeur et étroitement couplé à la webapp ou couche de modèle)

J'essaie moi-même d'écrire test si possible et de développer tout en testant mon code. Mais je le fais souvent à partir d'une approche descendante (test d'intégration) et j'essaie de trouver le bon point, où faire la "coupe de la couche d'application" pour les tests importants (souvent dans la couche modèle). (car il s'agit souvent de "calques")

De plus, le code de test unitaire et d'intégration ne vient pas sans impact négatif sur le temps, l'argent, la maintenance, le codage, etc. Il est cool et doit être appliqué, mais avec soin et en tenant compte des implications lors du démarrage ou après 5 ans de nombreux tests développés code.

Je dirais donc vraiment dépend beaucoup de la confiance et de l'évaluation des coûts/risques/avantages ... comme dans la vraie vie où vous ne pouvez pas et ne voulez pas courir avec beaucoup ou 100 % mécanismes de sécurité.

L'enfant peut et doit grimper quelque part et peut tomber et se blesser. La voiture peut cesser de fonctionner car j'ai fait le plein de carburant (entrée invalide :)). Le toast peut être brûlé si le bouton de temps a cessé de fonctionner après 3 ans. Mais je ne veux jamais conduire sur l'autoroute et tenir mon volant détaché dans mes mains :)

0
Andreas Dietrich

À propos des aspects de conception: je crois que même les petits projets bénéficient de rendre le code testable. Vous n'avez pas nécessairement à introduire quelque chose comme Guice (une simple classe d'usine le fera souvent), mais séparer le processus de construction de la logique de programmation se traduit par

  • documenter clairement les dépendances de chaque classe via son interface (très utile pour les nouveaux venus dans l'équipe)
  • les classes devenant beaucoup plus claires et plus faciles à maintenir (une fois que vous avez mis la création de graphes d'objets laids dans une classe séparée)
  • couplage lâche (rendant les changements beaucoup plus faciles)
0
Oliver Weiler