web-dev-qa-db-fra.com

Comment les tests unitaires doivent-ils être écrits sans se moquer de manière approfondie?

Si je comprends bien, le but des tests unitaires est de tester des unités de code isolément . Cela signifie que:

  1. Ils ne doivent pas casser par tout code non lié changer ailleurs dans la base de code.
  2. Un seul test unitaire doit être interrompu par un bogue dans l'unité testée, par opposition aux tests d'intégration (qui peuvent se briser en tas).

Tout cela implique que toutes les dépendances extérieures d'une unité testée doivent être simulées. Et je veux dire toutes les dépendances extérieures, pas seulement les "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, que pratiquement chaque test unitaire doit se moquer . D'un autre côté, une recherche rapide sur Google à propos de la moquerie révèle des tonnes d'articles qui prétendent que "la moquerie est une odeur de code" et devrait surtout (mais pas complètement) être évitée.

Passons maintenant aux questions.

  1. Comment les tests unitaires doivent-ils être écrits correctement?
  2. Où se situe exactement la frontière 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 le Person.calculate la méthode sans se moquer de la dépendance Calculator (étant donné que la Calculator est une classe légère qui n'accède pas au "monde extérieur") est-elle considérée comme un test unitaire?

82
Alexander Lomia

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

Martin Fowler sur Test unitaire

Les tests unitaires sont souvent évoqués dans le développement de logiciels, et c'est un terme que je connais tout au long de mes programmes d'écriture. Comme la plupart des terminologies de développement logiciel, cependant, elle est très mal définie, et je constate que la confusion peut souvent se produire 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 Développement piloté par les tests, par exemple

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

Toute affirmation donnée selon laquelle "le point des tests unitaires est" 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 si vous vous limitez à un style qui teste chaque unité isolément, alors beaucoup de test double est inévitable conclusion.

Les conseils contradictoires que vous voyez proviennent de personnes opérant selon un ensemble différent d'hypothèses.

Par exemple, si vous écrivez des tests pour soutenir les développeurs pendant le processus de refactoring, et diviser une unité en deux est un refactoring qui devrait être pris en charge, alors quelque chose doit donner. Peut-être que ce type de test a besoin d'un nom différent? Ou peut-être avons-nous besoin d'une compréhension différente de "l'unité".

Vous pouvez 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 vraiment, ce sont les propriétés .

Lorsque j'introduis des modifications dans le code, je ne me soucie pas de l'isolement des tests - je sais déjà que "l'erreur" se trouve quelque part dans ma pile actuelle de modifications non vérifiées. Si je lance les tests fréquemment, je limite la profondeur de cette pile et trouver l'erreur est trivial (dans le cas extrême, les tests sont exécutés après chaque édition - la profondeur maximale de la pile est de un). Mais l'exécution des tests n'est pas l'objectif - c'est une interruption - il est donc utile de réduire l'impact de l'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 compris comment le faire dans mes circonstances).

Si vous appelez Calculator::add n'augmente pas de manière significative le temps nécessaire pour exécuter le test (ou l'une des autres propriétés importantes pour ce cas d'utilisation), alors je ne prendrais pas la peine d'utiliser un test double - il n'offre pas d'avantages supérieurs aux coûts .

Remarquez les deux hypothèses ici: un être humain dans le cadre de l'évaluation des coûts et la courte pile de changements non vérifiés dans l'évaluation des avantages. Dans des circonstances où ces conditions ne sont pas réunies, la valeur de "l'isolement" change un peu.

Voir aussi Hot Lava , par Harry Percival.

59
VoiceOfUnreason

Comment les tests unitaires doivent-ils être écrits sans se moquer de manière approfondie?

En minimisant les effets secondaires dans votre code.

En prenant votre exemple de code, si calculator par exemple parle à une API Web, alors soit vous créez des tests fragiles qui dépendent de la possibilité d'interagir avec cette API Web, soit vous en créez une maquette. Si toutefois c'est un ensemble de fonctions de calcul déterministe et sans état, alors vous ne devez pas (et ne devriez pas) vous en moquer. Si vous le faites, vous risquez que votre maquette se comporte différemment du vrai code, ce qui entraîne des bogues dans vos tests.

Les maquettes 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 URL, etc. qui dépendent de l'environnement dans lequel vous exécutez; ou qui sont de nature hautement étatique et non déterministe. Donc, si vous gardez ces parties du code au minimum et que vous les cachez derrière des abstractions, elles sont faciles à simuler et le reste de votre code évite d'avoir besoin de simulations.

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. Donc, vous voudrez peut-être les exécuter uniquement pendant la nuit sur un serveur CI, plutôt qu'à chaque fois que vous enregistrez et créez votre code. Les anciens tests doivent cependant être exécutés aussi souvent que possible. Quant à savoir si chaque test est alors un test unitaire ou d'intégration devient académique et évite les "guerres enflammées" sur ce qui est et n'est pas un test unitaire.

40
David Arno

Ces questions sont assez 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 ne unité (méthode ou classe) et n'utilise d'autres unités que dans la mesure nécessaire pour atteindre cet objectif. La moquerie peut être nécessaire, mais ce n'est pas le but du test. Un test d'intégration teste l'interaction entre différentes unités réelles. Cette différence est la raison pour laquelle nous avons besoin de tests unitaires et d'intégration - si l'un faisait assez 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 des outils spécialisés plutôt qu'un outil généralisé.

Maintenant, pour la question importante: comment devez-vous tester unitaire? Comme indiqué ci-dessus, les tests unitaires devraient construire des structures auxiliaires niquement dans la mesure du nécessaire. Il est souvent plus facile d'utiliser une base de données factice que votre base de données réelle ou même n'importe laquelle base de données réelle. Cependant, se moquer en soi n'a aucune valeur. S'il arrive souvent qu'il soit en fait plus facile d'utiliser réel les composants d'une autre couche comme entrée pour un test unitaire de niveau intermédiaire. Si oui, n'hésitez pas à les utiliser.

De nombreux praticiens ont peur que si le test unitaire B réutilise des classes qui avaient déjà été testées par le test unitaire A, alors un défaut dans l'unité A provoque des échecs de test à plusieurs endroits. Je considère que ce n'est pas un problème: une suite de tests doit réussir 100% afin de vous rassurer, ce n'est donc pas un gros problème d'avoir trop d'échecs - après tout, vous - do a un défaut. Le seul problème critique serait si un défaut se déclenchait également pe échecs.

Par conséquent, ne faites pas une religion de moquerie. C'est un moyen, pas une fin, donc si vous parvenez à éviter l'effort supplémentaire, vous devriez le faire.

30
Kilian Foth

OK, donc pour répondre 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 frontière 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 moquées.

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 simulé, c'est un test unitaire, si vous 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 la suivante:

une recherche rapide sur Google à propos de la moquerie révèle des tonnes d'articles qui affirment que "la moquerie est une odeur de code" et devrait être évitée la plupart du temps (mais pas complètement).

Je pense que le problème ici est que beaucoup de gens utilisent des simulateurs pour recréer complètement les dépendances. Par exemple, je pourrais me moquer de la calculatrice dans votre exemple comme

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 l'implémentation". Je dirais "n'écrivez pas de moqueries! * comme ça".

D'autres personnes seraient en désaccord avec moi, nous déclencherions des guerres de flammes massives sur nos blogs sur la meilleure façon de se moquer, ce qui n'aurait vraiment aucun sens à moins que vous ne compreniez tout le contexte des différentes approches et n'offriez vraiment pas beaucoup de valeur à quelqu'un qui veut juste écrire de bons tests.

7
Ewan
  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 par rapport aux interfaces, pas aux implémentations . Cela présente de nombreux avantages. D'une part, il garantit que vos classes suivent le Dependency Inversion Principle de SOLID . C'est aussi ce que font vos autres classes ( non?) donc vos tests devraient 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 changeraient).
  • Sont autonomes . Comme vous l'avez dit, les modifications de tout code extérieur ne peuvent pas affecter le résultat du test. En tant que tels, les tests unitaires peuvent s'exécuter au moment de la construction. Cela signifie que vous avez besoin de simulacres pour supprimer tout effet secondaire. Cependant, si vous suivez le principe d'inversion de dépendance, cela devrait être relativement facile. De bons cadres de test comme Spock peuvent être utilisés pour fournir dynamiquement des implémentations simulées 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 les classes de modèle ["beans"]).
  • Ne nécessite pas d'application distincte en cours d'exécution . Si le test doit "parler à quelque chose", qu'il s'agisse d'une base de données ou d'un service Web, c'est un test d'intégration, pas 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, est un jeu équitable à mon avis pour un test unitaire si vous en avez vraiment besoin.

S'il existe des classes d'utilitaires provenant de 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 la simulation de ces dépendances. Ces emballages ne seraient alors pas nécessairement soumis à des tests unitaires.

  1. Où se situe exactement la frontière entre les [tests unitaires] et les tests d'intégration?

J'ai trouvé que cette distinction était la plus utile:

  • Les tests unitaires simulent le "code utilisateur" , vérifiant le comportement des classes d'implémentation par rapport au comportement et à la sémantique souhaités de interfaces au niveau du code =.
  • Les tests d'intégration simulent l'utilisateur , vérifiant le comportement de l'application en cours d'exécution 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 qu'étape finale d'une génération, puis détruire le conteneur par la suite, est-il correct d'inclure ces tests en tant que "tests unitaires"? Si c'est votre débat brûlant, vous êtes plutôt bien placé.

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

Non. Certains cas de test individuels concerneront des conditions d'erreur, comme passer null comme paramètre et vérifier que vous obtenez une exception. De nombreux tests comme celui-ci ne nécessiteront aucune simulation. En outre, les implémentations qui n'ont aucun effet secondaire, par exemple le traitement de chaîne ou les fonctions mathématiques, peuvent ne pas nécessiter de simulation car vous vérifiez simplement la sortie. Mais la plupart des classes qui valent la peine, je pense, 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 écrire vos tests. C'est un indice dont vous avez besoin pour refactoriser l'implémentation et diviser les choses, 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 unitaire doit être interrompu par un bug dans l'unité testée.

Je ne pense pas que ce soit une attente raisonnable, car cela fonctionne contre la réutilisation. Vous pouvez avoir une méthode private, par exemple, qui est appelée par plusieurs méthodes public publiées par votre interface. Un bogue introduit dans cette méthode pourrait alors entraîner plusieurs échecs de test. Cela ne signifie pas que vous devez copier le même code dans chaque méthode public.

4
wberry
  1. Ils ne doivent pas casser par tout code non lié changer ailleurs dans la base de code.

Je ne sais pas vraiment en quoi cette règle est utile. Si un changement dans une classe/méthode/quoi que ce soit peut briser le comportement d'une autre dans le code de production, alors les choses sont, en réalité, des collaborateurs, et non sans rapport. Si vos tests échouent et que votre code de production ne le fait pas, alors vos tests sont suspects.

  1. Un seul test unitaire doit être interrompu par un bogue dans l'unité testée, par opposition aux tests d'intégration (qui peuvent se briser en 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 un échec de test unitaire, alors vous dites que vous avez déjà identifié tous les bogues potentiels, même si la base de code évolue pour utiliser les cas que vous n'ont pas prévu.

Où se situe exactement la frontière 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 à partir desquels vous pouvez écrire des tests qui `` ont du sens '' en termes de domaine problématique/de règles métier qui ce niveau du code traite. Souvent, ces tests sont de nature quelque peu "fonctionnelle" - insérés dans une entrée et testant que la sortie est conforme aux attentes. Si les tests expriment un comportement souhaité du système, ils restent souvent assez stables même lorsque le code de production évolue et est refactorisé.

Comment les tests unitaires doivent-ils être écrits sans se moquer de manière approfondie?

Ne lisez pas trop dans l'unité Word, et penchez-vous vers l'utilisation de vos classes de production réelles dans les tests, sans trop vous inquiéter si vous impliquez plusieurs d'entre elles dans un test. Si l'un d'eux est difficile à utiliser (car il faut beaucoup d'initialisation, ou il doit frapper une vraie base de données/serveur de messagerie, etc.), alors laissez vos pensées se tourner vers la moquerie/la contrefaçon.

3

Tout d'abord, quelques définitions:

n test unitaire teste les unités isolément des autres unités, mais ce que cela signifie n'est défini concrètement par aucune source faisant autorité, alors définissons-le un peu mieux: si les limites d'E/S sont franchies (que ce soit/O est l'entrée réseau, disque, écran ou interface utilisateur), il y a un endroit semi-objectif où nous pouvons tracer une ligne. Si le code dépend des E/S, il traverse une frontière d'unité, et il devra donc 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 comme les fonctions pures, ce qui signifie que les tests unitaires se prêtent aux fonctions pures ou aux fonctions sans effets secondaires.

Si vous voulez tester des unités avec des effets, les unités responsables des effets doivent être moquées, mais peut-être devriez-vous plutôt 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 y a une meilleure réponse, plus longue ici, et le trou du lapin va beaucoup plus loin. Les mocks peuvent être mon odeur de code préférée car il y a tellement de choses à apprendre d'eux.

Le code sent

Pour cela, nous allons nous tourner 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 profond.

Ça continue plus tard ...

"Les odeurs sont certaines structures du code qui indiquent une violation des principes de conception fondamentaux et un impact négatif sur la qualité de la conception". Suryanarayana, Girish (novembre 2014). Refactorisation des odeurs de conception de logiciels. Morgan Kaufmann. p. 258.

Les odeurs de code ne sont généralement pas des bogues; 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 qui peuvent ralentir le développement ou augmenter le risque de bogues 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 courantes que quelque chose pourrait ne pas être exprimé sous sa forme optimale, et l'odeur peut indiquer une opportunité d'améliorer le code en question.

Dans le cas de la moquerie, l'odeur indique que les unités qui semblent appeler des moqueries dépendent des unités à moquer. Cela peut être une indication que nous n'avons pas décomposé le problème en morceaux atomiquement solubles, et que pourrait indiquer un défaut de conception dans le logiciel.

L'essence de tout développement logiciel est le processus de décomposition d'un gros problème en morceaux plus petits et indépendants (décomposition) et de composition des solutions pour former une application qui résout le gros problème (composition).

La moquerie est requise lorsque les unités utilisées pour décomposer le gros problème en parties plus petites dépendent les unes des autres. Autrement dit, la moquerie est nécessaire lorsque nos supposées unités atomiques de composition ne sont pas vraiment atomiques, et notre stratégie de décomposition n'a pas réussi à décomposer le problème plus grand en plus petit, indépendant problèmes à résoudre.

Ce qui fait que se moquer d'une odeur de code, ce n'est pas qu'il y ait quelque chose d'intrinsèquement mauvais avec la simulation - parfois c'est très utile. Ce qui en fait une odeur de code, c'est qu'il pourrait indiquer une source problématique de couplage 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 maquettes sont une odeur de code peut vous apprendre à identifier et à éviter les pires types tôt dans le cycle de vie de la conception de l'application, avant l'odeur se développe en quelque chose de pire.

2
Eric Elliott

La moquerie ne doit être utilisée 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 de code qui a du sens, quel que soit votre nom. Un élément important pour avoir un code bien testé est de pouvoir refactoriser librement, et une partie de pouvoir refactoriser librement signifie que vous n'avez pas à modifier vos tests pour le faire. Plus vous vous moquez, plus vous devez changer vos tests lorsque vous refactorisez. Si vous considérez la méthode comme l'unité, vous devez changer vos tests chaque fois que vous refactorisez. Et si vous considérez la classe comme l'unité, vous devez changer vos tests chaque fois que vous voulez diviser une classe en plusieurs classes. Lorsque vous devez refactoriser vos tests afin de refactoriser votre code, cela fait que 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, ou vous allez vous retrouver avec des classes de spaghetti surdimensionnées de 500 lignes. 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 en dehors. Si c'était le cas, vous devriez vous moquer du cours de mathématiques de votre langue, et absolument 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 avez confiance qu'ils sont bien testés et fonctionnent comme ils sont censés le faire. La seule vraie différence est que si vos dépendances internes cassent vos modules, vous pouvez arrêter ce que vous faites pour le corriger plutôt que d'avoir à publier un problème sur GitHub et soit creuser dans une base de code que vous ne comprenez pas pour le corriger ou espérer pour le mieux.

Isoler votre code signifie simplement que vous traitez vos dépendances internes comme des boîtes noires et ne testez pas les choses qui se passent à l'intérieur. Si vous avez le module B qui accepte les entrées 1, 2 ou 3, et que vous avez le module A, qui l'appelle, vous n'avez pas vos tests pour le module A faire chacune de ces options, vous en choisissez simplement une et utilisez-la. Cela signifie que vos tests pour le module A doivent tester les différentes façons dont vous traitez les réponses du module B, pas les choses que vous y passez.

Donc, si votre contrôleur transmet un objet complexe à une dépendance, et que cette dépendance fait plusieurs choses possibles, peut-être l'enregistrer dans la base de données et peut-être renvoyer une variété d'erreurs, mais tout ce que votre contrôleur fait est simplement de vérifier s'il retourne une erreur ou non et transmettez ces informations, alors tout ce que vous testez dans votre contrôleur est un test pour savoir s'il renvoie une erreur et le transmet et un test pour 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 ce serait un test d'intégration. Vous n'avez pas à vous moquer de la dépendance pour ce faire. Vous avez isolé le code.

1
TiggerToo