Si j'ai bien compris, le but des tests unitaires est de tester les unités de code isolément . Cela signifie que:
- Ils ne doivent pas interrompre par un changement de code non lié ailleurs dans la base de code.
- Un seul test unitaire devrait casser un bogue dans l'unité testée, par opposition aux tests d'intégration (qui peuvent casser des tas).
Tout cela implique que toutes les dépendances extérieures d'une unité testée doivent être simulées. Et je parle de toutes les dépendances extérieures , pas seulement des "couches extérieures" telles que la mise en réseau, le système de fichiers, la base de données, etc.
Cela conduit à une conclusion logique, à laquelle pratiquement chaque test unitaire doit se moquer . D'autre part, une recherche rapide dans Google sur les moqueries révèle des tonnes d'articles qui prétendent que "se moquer est une odeur de code" et qu'il convient dans la plupart des cas (mais pas complètement) d'être évité.
Maintenant, aux questions.
- Comment les tests unitaires doivent-ils être écrits correctement?
- Où se situe exactement la ligne entre eux et les tests d'intégration?
Mise à jour 1
Veuillez considérer le pseudo code suivant:
class Person {
constructor(calculator) {}
calculate(a, b) {
const sum = this.calculator.add(a, b);
// do some other stuff with the `sum`
}
}
Un test qui teste la Person.calculate
méthode sans se moquer de la Calculator
dépendance (étant donné qu’il Calculator
s’agit d’une classe légère qui n’accède pas au monde extérieur) peut-il être considéré comme un test unitaire?
la source
Réponses:
Martin Fowler sur le test unitaire
Ce que Kent Beck a écrit dans Test Driven Development, par exemple
Toute revendication du "critère des tests unitaires" dépendra fortement de la définition du "test unitaire" envisagée.
Si votre point de vue est que votre programme est composé de nombreuses petites unités qui dépendent les unes des autres et que vous vous limitez à un style qui teste chaque unité de manière isolée, de nombreux tests en double sont une conclusion inévitable.
Les avis contradictoires que vous voyez proviennent de personnes qui opèrent selon un ensemble d'hypothèses différent.
Par exemple, si vous écrivez des tests pour aider les développeurs pendant le processus de refactorisation, et que diviser une unité en deux est une refactorisation qui doit être prise en charge, il faut donner quelque chose. Peut-être que ce genre de test nécessite un nom différent? Ou peut-être avons-nous besoin d'une compréhension différente de «l'unité».
Vous voudrez peut-être comparer:
Je pense que ce n'est pas la bonne question à poser; C'est encore un argument sur les étiquettes , quand je crois que ce qui nous intéresse, ce sont les propriétés .
Lorsque j'apporte des modifications au code, l'isolement des tests m'importe peu: je sais déjà que "l'erreur" se situe quelque part dans ma pile actuelle de modifications non vérifiées. Si j'exécute fréquemment les tests, je limite la profondeur de la pile et la recherche de l'erreur est triviale (dans le cas extrême, les tests sont exécutés après chaque édition (la profondeur maximale de la pile est égale à un). Mais exécuter les tests n’est pas l’objectif, c’est une interruption, il est donc utile de réduire l’impact de cette interruption. Une façon de réduire l'interruption est de s'assurer que les tests sont rapides ( Gary Bernhardt suggère 300 ms , mais je n'ai pas trouvé comment le faire dans mon cas).
Si l'invocation
Calculator::add
n'augmente pas de manière significative le temps requis pour exécuter le test (ou l'une des autres propriétés importantes pour ce cas d'utilisation), je ne prendrais donc pas la peine d'utiliser un test double: il ne procure pas d'avantages dépassant les coûts. .Notez les deux hypothèses suivantes: un être humain dans le cadre de l'évaluation des coûts et la pile de modifications non vérifiées dans l'évaluation des avantages. Dans les cas où ces conditions ne sont pas remplies, la valeur de "l'isolement" change un peu.
Voir aussi Hot Lava , de Harry Percival.
la source
En minimisant les effets secondaires dans votre code.
Si vous prenez votre exemple de code si,
calculator
par exemple, vous parlez à une API Web, vous créez des tests fragiles reposant sur la possibilité d’interagir avec cette API Web ou vous en créez une maquette. Si, toutefois, il s'agit d'un ensemble de fonctions de calcul dépourvu d'état et déterministe, vous ne devez pas (et ne devriez pas) vous moquer de lui. Si vous le faites, vous risquez que votre comportement fictif se comporte différemment du code réel, ce qui entraînerait des erreurs dans vos tests.Des simulations ne devraient être nécessaires que pour le code qui lit / écrit dans le système de fichiers, les bases de données, les points de terminaison d'URL, etc. qui dépendent de l'environnement dans lequel vous vous trouvez; ou qui sont très dynamiques et de nature non déterministe. Donc, si vous gardez au minimum ces parties du code et que vous les cachez derrière des abstractions, elles sont faciles à simuler et le reste de votre code évite de recourir à des simulacres.
Pour les points de code qui ont des effets secondaires, cela vaut la peine d'écrire des tests qui se moquent et des tests qui n'en ont pas. Ces derniers ont cependant besoin de soins, car ils seront intrinsèquement fragiles et éventuellement lents. Il est donc préférable de ne les exécuter que du jour au lendemain sur un serveur CI, plutôt que chaque fois que vous enregistrez et créez votre code. Les tests précédents doivent cependant être exécutés aussi souvent que possible. Quant à savoir si chaque test est un test unitaire ou un test d'intégration, il devient théorique et évite les "guerres de flammes" sur ce qui est ou non un test unitaire.
la source
R.equals
). Comme ce sont pour la plupart des fonctions pures, elles ne sont généralement pas simulées lors des tests.Ces questions sont très différentes dans leur difficulté. Prenons d'abord la question 2.
Les tests unitaires et les tests d'intégration sont clairement séparés. Un test unitaire teste une unité (méthode ou classe) et utilise les autres unités uniquement dans la mesure nécessaire pour atteindre cet objectif. Se moquer peut être nécessaire, mais ce n'est pas le point du test. Un test d'intégration teste l'interaction entre différentes unités réelles . Cette différence est la raison même pour laquelle nous avons besoin de tests unitaires et d'intégration - si l'un fait suffisamment bien le travail de l'autre, nous ne le ferions pas, mais il s'est avéré qu'il est généralement plus efficace d'utiliser deux outils spécialisés plutôt qu'un outil généralisé. .
Passons maintenant à la question importante: comment devriez-vous tester l’unité? Comme indiqué ci-dessus, les tests unitaires ne devraient construire des structures auxiliaires que dans la mesure nécessaire . Il est souvent plus facile d'utiliser une base de données fictive que votre vraie base de données ou même toute base de données réelle. Cependant, se moquer de lui-même n'a aucune valeur. Si cela arrive souvent, il est en fait plus facile d'utiliser les composants réels d'une autre couche en tant qu'entrée pour un test unitaire de niveau intermédiaire. Si oui, n'hésitez pas à les utiliser.
De nombreux praticiens craignent que si le test unitaire B réutilise des classes déjà testées par le test unitaire A, un défaut de l’unité A provoque des échecs de test à plusieurs endroits. Je considère que ce pas un problème: une suite de test doit réussir 100% afin de vous donner l'assurance dont vous avez besoin, il est donc pas un gros problème d'avoir trop d'échecs - après tout, vous faire un défaut. Le seul problème critique serait si un défaut entraînait trop peu de pannes.
Par conséquent, ne faites pas une religion moqueuse. C'est un moyen, pas une fin, alors si vous pouvez vous en tirer en évitant l'effort supplémentaire, vous devriez le faire.
la source
The only critical problem would be if a defect triggered too few failures.
c'est l'un des points faibles de la moquerie. Nous devons "programmer" le comportement attendu afin que nous puissions échouer, ce qui entraînerait la fin de nos tests en tant que "faux positifs". Mais se moquer est une technique très utile pour atteindre le déterminisme (la condition la plus importante du test). Je les utilise dans tous mes projets lorsque cela est possible. Ils me montrent également lorsque l'intégration est trop complexe ou la dépendance trop étroite.OK, alors répondez directement à vos questions:
Comme vous le dites, vous devriez vous moquer des dépendances et tester uniquement l'unité en question.
Un test d'intégration est un test unitaire dans lequel vos dépendances ne sont pas fausse.
Non. Vous devez injecter la dépendance de la calculatrice dans ce code et vous avez le choix entre une version simulée ou une version réelle. Si vous utilisez un simulateur, c'est un test unitaire, si vous en utilisez un réel, c'est un test d'intégration.
Cependant, une mise en garde. vous souciez-vous vraiment de ce que les gens pensent que vos tests devraient être appelés?
Mais votre vraie question semble être celle-ci:
Je pense que le problème ici est que beaucoup de gens utilisent des simulacres pour recréer complètement les dépendances. Par exemple, je pourrais me moquer de la calculatrice dans votre exemple
Je ne ferais pas quelque chose comme:
Je dirais que ce serait "tester ma maquette" ou "tester la mise en œuvre". Je dirais " N'écris pas Mocks! * Comme ça".
D'autres personnes seraient en désaccord avec moi, nous lancerions d'énormes guerres de flammes sur nos blogs à propos du meilleur moyen de nous moquer, ce qui n'aurait aucun sens si vous ne compreniez pas le contexte des différentes approches et n'offriez vraiment pas beaucoup de valeur. à quelqu'un qui veut juste écrire de bons tests.
la source
MockCalc
àStubCalc
, et l' appeler un talon pas une maquette. martinfowler.com/articles/…Ma règle d'or est que les tests unitaires appropriés:
S'il existe des classes d'utilitaires dans les frameworks qui compliquent les tests unitaires, vous pouvez même trouver utile de créer des interfaces et des classes "wrapper" très simples pour faciliter le moquage de ces dépendances. Ces emballages ne seraient alors pas nécessairement soumis à des tests unitaires.
J'ai trouvé cette distinction la plus utile:
Il y a une zone grise ici. Par exemple, si vous pouvez exécuter une application dans un conteneur Docker et exécuter les tests d'intégration en tant que dernière étape d'une construction, puis détruire le conteneur, est-il correct d'inclure ces tests en tant que "tests unitaires"? Si ceci est votre débat brûlant, vous êtes dans un très bon endroit.
Certains scénarios de test individuels concernent des conditions d'erreur, telles que le passage en
null
tant que paramètre et la vérification de l'obtention d'une exception. De nombreux tests comme celui-ci ne nécessitent pas de simulacres. De plus, les implémentations qui n'ont pas d'effets secondaires, par exemple le traitement de chaînes ou les fonctions mathématiques, peuvent ne nécessiter aucune simulation, car vous vérifiez simplement la sortie. Mais je pense que la plupart des cours qui en valent la peine nécessiteront au moins une maquette quelque part dans le code de test. (Moins il y en a, mieux c'est.)Le problème "d'odeur de code" que vous avez mentionné se pose lorsque vous avez une classe trop compliquée, qui nécessite une longue liste de dépendances factices pour pouvoir écrire vos tests. Ceci est un indice qui vous oblige à reformuler la mise en œuvre et à la scinder, afin que chaque classe ait une empreinte plus petite et une responsabilité plus claire, et soit donc plus facilement testable. Cela améliorera la qualité à long terme.
Je ne pense pas que ce soit une attente raisonnable, car cela va à l’encontre de la réutilisation. Vous pouvez avoir une
private
méthode, par exemple, appelée par plusieurspublic
méthodes publiées par votre interface. Un bogue introduit dans cette méthode peut alors provoquer plusieurs échecs de test. Cela ne signifie pas que vous devriez copier le même code dans chaquepublic
méthode.la source
Je ne suis pas vraiment sûr de l'utilité de cette règle. Si un changement dans une classe / méthode / quoi que ce soit puisse briser le comportement d'une autre dans le code de production, alors les choses sont, en réalité, des collaborateurs et non des relations. Si vos tests échouent et que votre code de production ne le fait pas, alors vos tests sont suspects.
Je considérerais cette règle avec suspicion aussi. Si vous êtes vraiment assez bon pour structurer votre code et écrire vos tests de telle sorte qu'un bogue provoque exactement l'échec d'un test unitaire, vous dites alors que vous avez déjà identifié tous les bogues potentiels, même si la base de code évolue pour vous utiliser dans des cas d'utilisation similaires. n'ont pas anticipé.
Je ne pense pas que ce soit une distinction importante. Qu'est-ce qu'une "unité" de code de toute façon?
Essayez de trouver des points d’entrée où vous pouvez écrire des tests qui «ont un sens» en termes de domaine / règles métier posant problème et traités par ce niveau de code. Ces tests sont souvent de nature «fonctionnelle» - insérez une entrée et testez que la sortie est celle attendue. Si les tests expriment un comportement souhaité du système, ils restent souvent assez stables même si le code de production évolue et est refactoré.
Ne lisez pas trop le mot 'unité' et ne vous orientez pas vers l'utilisation de vos véritables classes de production dans les tests, sans trop vous inquiéter si vous impliquez plus d'un d'entre eux dans un test. Si l’un d’eux est difficile à utiliser (parce qu’il nécessite beaucoup d’initialisation, ou qu’il faut s’appuyer sur une vraie base de données / serveur de messagerie, etc.), laissez votre pensée tourner à la moquerie.
la source
Person:tellStory()
méthode qui incorpore les détails d'une personne dans une chaîne, puis la retourne, alors "l'histoire" est probablement une unité. Si je crée une méthode d'aide privée qui supprime une partie du code, je ne pense pas avoir introduit une nouvelle unité - je n'ai pas besoin de tester cela séparément.Tout d'abord, quelques définitions:
Une unité teste les unités isolément des autres unités, mais ce que cela signifie n'est défini de manière concrète par aucune source faisant autorité. Définissons-le donc un peu mieux: si les limites d'E / S sont franchies (que cette E / S soit un réseau, un disque, écran, ou interface utilisateur), il y a un endroit semi-objectif sur lequel on peut tracer une ligne. Si le code dépend des E / S, il franchit les limites d'une unité et doit par conséquent se moquer de l'unité responsable de ces E / S.
Selon cette définition, je ne vois pas de raison impérieuse de se moquer de choses telles que des fonctions pures, ce qui signifie que les tests unitaires se prêtent à des fonctions pures ou à des fonctions sans effets secondaires.
Si vous voulez tester les unités avec effets, vous devez vous moquer des unités responsables des effets, mais vous devriez peut-être envisager un test d'intégration. Donc, la réponse courte est: "si vous avez besoin de vous moquer, demandez-vous si vous avez vraiment besoin d'un test d'intégration." Mais il existe une meilleure réponse plus longue ici, et le terrier du lapin va beaucoup plus loin. Mock peut être mon odeur de code préférée car il y a tant à apprendre d'eux.
Code odeurs
Pour cela, nous nous tournons vers Wikipedia:
Ça continue plus tard ...
En d'autres termes, toutes les odeurs de code ne sont pas mauvaises. Au lieu de cela, ce sont des indications communes que quelque chose pourrait ne pas être exprimé dans sa forme optimale, et l'odeur peut indiquer une opportunité d'améliorer le code en question.
Dans le cas de moqueries, l'odeur indique que les unités qui semblent appeler à se moquer dépendent des unités à simuler. Cela peut indiquer que nous n'avons pas décomposé le problème en éléments pouvant être résolus sur le plan atomique, ce qui pourrait indiquer un défaut de conception dans le logiciel.
L’essence de tout développement logiciel est le processus qui consiste à décomposer un gros problème en morceaux plus petits et indépendants (décomposition) et à composer les solutions ensemble pour former une application qui résout le gros problème (composition).
Le moquage est nécessaire lorsque les unités utilisées pour décomposer le gros problème en parties plus petites dépendent les unes des autres. En d'autres termes, il est nécessaire de se moquer lorsque nos unités de composition atomiques supposées ne sont pas réellement atomiques et que notre stratégie de décomposition n'a pas réussi à décomposer le problème plus vaste en problèmes plus petits et indépendants à résoudre.
Ce qui rend moqueur une odeur de code, ce n’est pas qu’il ya quelque chose de fondamentalement faux à se moquer, il est parfois très utile. Ce qui en fait une odeur de code, c'est qu'il pourrait indiquer une source de couplage problématique dans votre application. Parfois, supprimer cette source de couplage est beaucoup plus productif que d'écrire une maquette.
Il existe de nombreux types de couplage, et certains sont meilleurs que d'autres. Comprendre que les simulacres sont une odeur de code peut vous apprendre à identifier et à éviter les pires types au tout début du cycle de vie d'une application, avant que l'odeur ne devienne pire.
la source
Les moqueries ne doivent être utilisées qu'en dernier recours, même dans les tests unitaires.
Une méthode n'est pas une unité et même une classe n'est pas une unité. Une unité est une séparation logique du code qui a du sens, peu importe comment vous l'appelez. Un élément important pour avoir du code bien testé est de pouvoir librement refactoriser, ce qui signifie que vous n'avez pas à changer vos tests pour le faire. Plus vous vous moquez, plus vous devez modifier vos tests lorsque vous effectuez une refactorisation. Si vous considérez la méthode comme une unité, vous devez modifier vos tests chaque fois que vous effectuez une refactorisation. Et si vous considérez la classe comme l'unité, vous devez modifier vos tests chaque fois que vous souhaitez diviser une classe en plusieurs classes. Lorsque vous devez refactoriser vos tests afin de refactoriser votre code, les gens choisissent de ne pas refactoriser leur code, ce qui est à peu près la pire chose qui puisse arriver à un projet. Il est essentiel que vous puissiez diviser une classe en plusieurs classes sans avoir à refactoriser vos tests, sinon vous allez vous retrouver avec des classes de spaghettis sur 500 lignes surdimensionnées. Si vous traitez des méthodes ou des classes comme vos unités avec des tests unitaires, vous ne faites probablement pas de programmation orientée objet mais une sorte de programmation fonctionnelle mutante avec des objets.
Isoler votre code pour un test unitaire ne signifie pas que vous vous moquez de tout. Si c'était le cas, vous devriez vous moquer du cours de mathématiques de votre langue et personne ne pense que c'est une bonne idée. Les dépendances internes ne doivent pas être traitées différemment des dépendances externes. Vous pensez qu'ils sont bien testés et fonctionnent comme ils sont supposés le faire. La seule différence réelle est que si vos dépendances internes détruisent vos modules, vous pouvez arrêter ce que vous faites pour le réparer plutôt que de devoir publier un problème sur GitHub et creuser dans une base de code que vous ne comprenez pas. ou espérer le meilleur.
Isoler votre code signifie simplement que vous traitez vos dépendances internes comme des boîtes noires et ne testez pas ce qui se passe à l'intérieur de celles-ci. Si vous avez le module B qui accepte des entrées de 1, 2 ou 3 et que vous avez le module A qui l'appelle, vous n'avez pas les tests pour le module A pour chacune de ces options, il vous suffit d'en choisir une et de l'utiliser. Cela signifie que vos tests pour le module A doivent vérifier les différentes manières dont vous traitez les réponses du module B, et non les éléments que vous y transmettez.
Donc, si votre contrôleur passe un objet complexe à une dépendance et que cette dépendance a plusieurs possibilités, vous pouvez l'enregistrer dans la base de données et peut-être renvoyer diverses erreurs, mais tout ce que fait votre contrôleur est simplement de vérifier s'il renvoie Si vous transmettez cette information avec une erreur ou non, alors tout ce que vous testez dans votre contrôleur correspond à un test, s'il renvoie une erreur et la transmet, et un test, s'il ne renvoie pas d'erreur. Vous ne testez pas si quelque chose a été enregistré dans la base de données ou quel type d'erreur il s'agit, car il s'agirait d'un test d'intégration. Pour ce faire, vous n'avez pas à vous moquer de la dépendance. Vous avez isolé le code.
la source