Comment doit-on écrire les tests unitaires sans se moquer de façon excessive?

80

Si j'ai bien compris, le but des tests unitaires est de tester les unités de code isolément . Cela signifie que:

  1. Ils ne doivent pas interrompre par un changement de code non lié ailleurs dans la base de code.
  2. 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.

  1. Comment les tests unitaires doivent-ils être écrits correctement?
  2. 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.calculateméthode sans se moquer de la Calculatordépendance (étant donné qu’il Calculators’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?

Alexander Lomia
la source
10
Une partie de ceci est juste une expérience de conception qui viendra avec le temps. Vous apprendrez à structurer vos composants afin qu’ils ne comportent pas de nombreuses dépendances difficiles à simuler. Cela signifie que la testabilité doit être un objectif de conception secondaire de tout logiciel. Cet objectif est beaucoup plus facile à atteindre si les tests sont écrits avant ou en même temps que le code, par exemple en utilisant TDD et / ou BDD.
amon
33
Placez les tests rapides et fiables dans un dossier. Mettez des tests lents et potentiellement fragiles dans un autre. Exécutez les tests dans le premier dossier aussi souvent que possible (idéalement, chaque fois que vous mettez en pause la saisie et que le code est compilé, c'est l'idéal, mais tous les environnements de développement ne la prennent pas en charge). Exécutez moins souvent les tests plus lents (lorsque vous prenez une pause-café par exemple). Ne vous inquiétez pas des noms d'unité et d'intégration. Appelez-les vite et lentement si vous voulez. Ça n'a pas d'importance.
David Arno
6
"quasiment tous les tests unitaires doivent se moquer de" Ouais, alors? "Une recherche rapide dans Google sur les moqueries révèle des tonnes d'articles affirmant que" se moquer est une odeur de code "" ils ont tort .
Michael
13
@Michael Dire simplement «oui, donc» et déclarer faussement l'opinion contraire ne sont pas un excellent moyen d'aborder un sujet aussi controversé que celui-ci. Peut-être rédigez-vous une réponse et expliquez-vous pourquoi vous pensez que les moqueries devraient être omniprésentes, et peut-être pourquoi pensez-vous que des «tonnes d'articles» sont intrinsèquement fausses?
AJFaraday
6
Puisque vous n'avez pas cité "Moqueur, c'est une odeur de code", je ne peux que deviner que vous interprétez mal ce que vous lisez. Se moquer n'est pas une odeur de code. Le besoin d'utiliser la réflexion ou d'autres manigances pour injecter vos mockings est une odeur de code. La difficulté de se moquer est inversement proportionnelle à la qualité de la conception de votre API. Si vous pouvez écrire des tests unitaires simples et simples qui ne font que transmettre des simulacres aux constructeurs, vous le faites bien.
TKK

Réponses:

59

le point des tests unitaires est de tester les unités de code isolément.

Martin Fowler sur le test unitaire

On parle souvent de tests unitaires dans le développement de logiciels, et c'est un terme que je connais bien tout au long de mes programmes d'écriture. Comme la plupart des terminologies de développement logiciel, cependant, c'est très mal défini, et je vois que la confusion peut souvent survenir lorsque les gens pensent qu'elle est plus étroitement définie qu'elle ne l'est réellement.

Ce que Kent Beck a écrit dans Test Driven Development, par exemple

Je les appelle "tests unitaires", mais ils ne correspondent pas très bien à la définition acceptée des tests unitaires

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:

Un test qui teste la méthode Person.calculate sans se moquer de la dépendance de la calculatrice (étant donné que la calculatrice est une classe légère qui n’accède pas au monde extérieur) peut-il être considéré comme un test unitaire?

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::addn'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.

VoiceOfUnreason
la source
5
Une chose que se moquent bien d’ajouter est de prouver que la calculatrice peut être moquée, c’est-à-dire que la conception ne couple pas la personne et la calculatrice (bien que cela puisse également être vérifié de différentes manières)
jk.
40

Comment doit-on écrire les tests unitaires sans se moquer de façon excessive?

En minimisant les effets secondaires dans votre code.

Si vous prenez votre exemple de code si, calculatorpar 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.

David Arno
la source
8
C’est la bonne réponse à la fois dans la pratique et dans l’esquive d’un débat sémantique sans signification.
Jared Smith
Avez-vous un exemple de base de code open source non-triviale qui utilise un tel style tout en maintenant une bonne couverture de test?
Joeri Sebrechts
4
@ JoeriSebrechts tous les FP? Exemple
Jared Smith
Ce n’est pas ce que je recherche, c’est juste un ensemble de fonctions indépendantes les unes des autres, et non un système de fonctions qui s’appellent. Comment pouvez-vous éviter de construire des arguments complexes à une fonction dans le but de la tester, si cette fonction est l'une des meilleures? Par exemple la boucle de base d'un jeu.
Joeri Sebrechts
1
@ JoeriSebrechts hmm, soit je ne comprends pas ce que vous voulez, soit vous n'avez pas plongé assez profondément dans l'exemple que j'ai donné. Les fonctions ramda utilisent des appels internes partout dans leur source (par exemple R.equals). Comme ce sont pour la plupart des fonctions pures, elles ne sont généralement pas simulées lors des tests.
Jared Smith
31

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.

Kilian Foth
la source
4
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.
Laiv
1
Si une unité testée utilise d'autres unités, n'est-ce pas vraiment un test d'intégration? Parce que, par essence, cette unité testera l’interaction entre lesdites unités, exactement comme le ferait un test d’intégration.
Alexander Lomia
11
@AlexanderLomia: Comment appelleriez-vous une unité? Souhaitez-vous appeler "String" une unité aussi? Je le ferais, mais je ne songerais pas à m'en moquer.
Bart van Ingen Schenau
5
" 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 ". Voici le frotter. C'est votre définition d'un test unitaire. Le mien est assez différent. La distinction entre eux n'est donc "clairement séparée" que pour une définition donnée, mais la séparation varie d'une définition à l'autre.
David Arno
4
@Voo Après avoir travaillé avec de telles bases de code, bien que trouver le problème d'origine (en particulier si l'architecture décrit les choses que vous utiliseriez pour le déboguer) peut être une gêne, j'ai encore eu plus de problèmes de des tests pour continuer à fonctionner après la rupture du code utilisé.
James_pic
6

OK, alors répondez directement à vos questions:

Comment les tests unitaires doivent-ils être écrits correctement?

Comme vous le dites, vous devriez vous moquer des dépendances et tester uniquement l'unité en question.

Où se situe exactement la ligne entre eux et les tests d'intégration?

Un test d'intégration est un test unitaire dans lequel vos dépendances ne sont pas fausse.

Un test qui teste la méthode Person.calculate sans se moquer de la calculatrice peut-il être considéré comme un test unitaire?

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:

une recherche rapide dans Google sur les moqueries révèle des tonnes d'articles affirmant que "se moquer est une odeur de code" et devrait être évité (mais pas complètement).

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

public class MockCalc : ICalculator
{
     public Add(int a, int b) { return 4; }
}

Je ne ferais pas quelque chose comme:

myMock = Mock<ICalculator>().Add((a,b) => {return a + b;})
myPerson.Calculate()
Assert.WasCalled(myMock.Add());

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.

Ewan
la source
Merci pour une réponse exhaustive. Pour ce qui est de savoir ce que les autres pensent de mes tests - en fait, je veux éviter d'écrire des tests de semi-intégration, de semi-unités, qui tendent à devenir un gâchis non fiable au fur et à mesure de l'avancement du projet.
Alexander Lomia
aucun problème, je pense que le problème est que les définitions des deux choses ne sont pas approuvées à 100% par tout le monde.
Ewan
Je renommer votre classe MockCalcà StubCalc, et l' appeler un talon pas une maquette. martinfowler.com/articles/…
bdsl
@bdsl Cet article a 15 ans
Ewan
4
  1. Comment les tests unitaires doivent-ils être mis en œuvre correctement?

Ma règle d'or est que les tests unitaires appropriés:

  • Sont codés contre les interfaces, pas les implémentations . Cela présente de nombreux avantages. D'une part, cela garantit que vos classes suivent le principe d'inversion de dépendance de SOLID . En outre, c’est ce que font vos autres classes ( non? ). Vos tests doivent donc faire de même. En outre, cela vous permet de tester plusieurs implémentations de la même interface tout en réutilisant une grande partie du code de test (seule l'initialisation et certaines assertions seraient modifiées).
  • Sont autonomes . Comme vous l'avez dit, toute modification dans un code externe ne peut affecter le résultat du test. En tant que tel, les tests unitaires peuvent s'exécuter au moment de la construction. Cela signifie que vous avez besoin de simulacres pour supprimer tous les effets secondaires. Cependant, si vous suivez le principe d'inversion de dépendance, cela devrait être relativement facile. De bons frameworks de test comme Spock peuvent être utilisés pour fournir de manière dynamique des implémentations factices de n'importe quelle interface à utiliser dans vos tests avec un minimum de codage. Cela signifie que chaque classe de test n'a besoin que d'exercer le code de exactement une classe d'implémentation, plus le framework de test (et peut-être des classes de modèle ["beans"]).
  • Ne nécessite pas d’application en cours d’exécution distincte . Si le test doit "communiquer avec quelque chose", qu'il s'agisse d'une base de données ou d'un service Web, il s'agit d'un test d'intégration et non d'un test unitaire. Je trace la ligne au niveau des connexions réseau ou du système de fichiers. Une base de données SQLite purement en mémoire, par exemple, constitue à mon avis un jeu juste pour un test unitaire si vous en avez vraiment besoin.

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.

  1. Où se situe exactement la ligne de démarcation entre les tests unitaires et les tests d'intégration?

J'ai trouvé cette distinction la plus utile:

  • Les tests unitaires simulent le "code utilisateur" , en vérifiant le comportement des classes d'implémentation par rapport au comportement souhaité et à la sémantique des interfaces de niveau code .
  • Les tests d'intégration simulent l'utilisateur en vérifiant le comportement de l' application en cours par rapport aux cas d'utilisation spécifiés et / ou aux API formelles. Pour un service Web, "l'utilisateur" serait une application cliente.

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.

  1. Est-il vrai que pratiquement chaque test unitaire doit se moquer?

Certains scénarios de test individuels concernent des conditions d'erreur, telles que le passage en nulltant 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.

Un seul test d'unité devrait casser par un bogue dans l'unité testée.

Je ne pense pas que ce soit une attente raisonnable, car cela va à l’encontre de la réutilisation. Vous pouvez avoir une privateméthode, par exemple, appelée par plusieurs publicmé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 chaque publicméthode.

Wberry
la source
3
  1. Ils ne doivent pas interrompre par un changement de code non lié ailleurs dans la base de code.

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.

  1. 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).

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é.

Où se situe exactement la ligne entre eux et les tests d'intégration?

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é.

Comment doit-on écrire les tests unitaires sans se moquer de façon excessive?

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.

topo morto
la source
" Qu'est-ce qu'une" unité "de code de toute façon? " Très bonne question qui peut avoir des réponses inattendues pouvant même dépendre de la personne qui répond. Généralement, la plupart des définitions de tests unitaires expliquent qu'elles se rapportent à une méthode ou à une classe, mais ce n'est pas une mesure vraiment utile d'une "unité" dans tous les cas. Si j'ai une 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.
VLAZ
2

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:

En programmation informatique, une odeur de code est une caractéristique du code source d’un programme qui indique éventuellement un problème plus grave.

Ça continue plus tard ...

"Les odeurs sont certaines structures du code qui indiquent une violation des principes de conception fondamentaux et ont une incidence négative sur la qualité de la conception". Suryanarayana, Girish (novembre 2014). Refactoring des odeurs de conception logicielle. Morgan Kaufmann. p. 258.

Les odeurs de code ne sont généralement pas des insectes; ils ne sont pas techniquement incorrects et n'empêchent pas le programme de fonctionner. Au lieu de cela, ils indiquent des faiblesses dans la conception susceptibles de ralentir le développement ou d’accroître les risques de bugs ou d’échecs à l’avenir.

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.

Eric Elliott
la source
1

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.

TiggerToo
la source