Quand devrais-je me moquer?

J’ai une compréhension de base des objects fictifs et faux, mais je ne suis pas sûr d’avoir une idée de quand / où utiliser la moquerie – en particulier car cela s’appliquerait à ce scénario ici .

Un test unitaire doit tester un seul chemin de code par une seule méthode. Lorsque l’exécution d’une méthode passe en dehors de cette méthode, dans un autre object, vous avez une dépendance.

Lorsque vous testez ce chemin de code avec la dépendance réelle, vous n’êtes pas un test unitaire; vous êtes test d’intégration. Bien que ce soit bien et nécessaire, ce ne sont pas des tests unitaires.

Si votre dépendance est boguée, votre test peut être affecté de manière à renvoyer un faux positif. Par exemple, vous pouvez transmettre la dépendance à un null inattendu et la dépendance peut ne pas générer de null, comme cela est documenté. Votre test n’aboutit pas à une exception d’argument null comme il se doit et le test réussit.

En outre, vous pouvez trouver difficile, voire impossible, de faire en sorte que l’object dépendant retourne exactement ce que vous voulez lors d’un test. Cela inclut également le lancement d’exceptions prévues dans les tests.

Un simulacre remplace cette dépendance. Vous définissez les attentes sur les appels à l’object dépendant, définissez les valeurs de retour exactes qu’il doit vous fournir pour effectuer le test souhaité et / ou les exceptions à lancer pour pouvoir tester votre code de gestion des exceptions. De cette façon, vous pouvez tester l’unité en question facilement.

TL; DR: se moque de chaque dépendance que votre unité touche.

Les objects de simulation sont utiles lorsque vous souhaitez tester des interactions entre une classe testée et une interface particulière.

Par exemple, nous voulons tester cette méthode sendInvitations(MailServer mailServer) appelle MailServer.createMessage() exactement une fois, et appelle également MailServer.sendMessage(m) une seule fois, et aucune autre méthode n’est appelée sur l’interface MailServer . C’est à ce moment que nous pouvons utiliser des objects fictifs.

Avec les objects simulés, au lieu de passer un vrai MailServerImpl , ou un test TestMailServer , nous pouvons passer une implémentation TestMailServer de l’interface MailServer . Avant de passer un simulacre de MailServer , nous le “formons” pour qu’il sache à quelle méthode les appels s’attendre et quelles valeurs renvoient. À la fin, l’object simulé affirme que toutes les méthodes attendues ont été appelées comme prévu.

Cela semble bien en théorie, mais il y a aussi des inconvénients.

Manque de défauts

Si vous avez un framework simulé en place, vous êtes tenté d’utiliser un object simulé à chaque fois que vous devez passer une interface à la classe dans le test. De cette façon, vous finissez par tester les interactions même lorsque cela n’est pas nécessaire . Malheureusement, les tests indésirables (accidentels) d’interactions sont mauvais, car vous testez alors qu’une exigence particulière est implémentée d’une manière particulière, au lieu que la mise en œuvre produise le résultat requirejs.

Voici un exemple de pseudocode. Supposons que nous ayons créé une classe MySorter et que nous voulions la tester:

 // the correct way of testing testSort() { testList = [1, 7, 3, 8, 2] MySorter.sort(testList) assert testList equals [1, 2, 3, 7, 8] } // incorrect, testing implementation testSort() { testList = [1, 7, 3, 8, 2] MySorter.sort(testList) assert that compare(1, 2) was called once assert that compare(1, 3) was not called assert that compare(2, 3) was called once .... } 

(Dans cet exemple, nous supposons que ce n’est pas un algorithme de sorting particulier, tel que le sorting rapide, que nous voulons tester; dans ce cas, le dernier test serait réellement valide.)

Dans un exemple aussi extrême, il est évident que ce dernier exemple est faux. Lorsque nous modifions l’implémentation de MySorter , le premier test fait un excellent travail en s’assurant que nous sortingons toujours correctement, ce qui est le point MySorter des tests – ils nous permettent de changer le code en toute sécurité. En revanche, ce dernier test est toujours rompu et il est activement nuisible; cela entrave le refactoring.

Les mecs comme des talons

Les frameworks de simulation permettent souvent une utilisation moins ssortingcte, où il n’est pas nécessaire de spécifier exactement combien de fois les méthodes doivent être appelées et quels sont les parameters attendus. ils permettent de créer des objects simulés utilisés comme des stubs .

Supposons que nous ayons une méthode sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer) que nous voulons tester. L’object PdfFormatter peut être utilisé pour créer l’invitation. Voici le test:

 testInvitations() { // train as stub pdfFormatter = create mock of PdfFormatter let pdfFormatter.getCanvasWidth() returns 100 let pdfFormatter.getCanvasHeight() returns 300 let pdfFormatter.addText(x, y, text) returns true let pdfFormatter.drawLine(line) does nothing // train as mock mailServer = create mock of MailServer expect mailServer.sendMail() called exactly once // do the test sendInvitations(pdfFormatter, mailServer) assert that all pdfFormatter expectations are met assert that all mailServer expectations are met } 

Dans cet exemple, nous ne nous soucions pas vraiment de l’object PdfFormatter , nous l’ PdfFormatter donc à accepter discrètement tout appel et à renvoyer des valeurs de retour sendInvitation() pour toutes les méthodes que sendInvitation() appelle à ce stade. Comment avons-nous trouvé exactement cette liste de méthodes à former? Nous avons simplement effectué le test et continué à append les méthodes jusqu’à ce que le test réussisse. Notez que nous avons formé le stub pour qu’il réponde à une méthode sans avoir la moindre idée de la nécessité de l’appeler, nous avons simplement ajouté tout ce dont il se plaignait. Nous sums heureux, le test réussit.

Mais que se passe-t-il plus tard, lorsque nous modifions sendInvitations() , ou une autre classe sendInvitations() par sendInvitations() , pour créer davantage de fichiers PDF fantaisie? Notre test a soudainement échoué parce que maintenant plus de méthodes de PdfFormatter sont appelées et nous n’avons pas entraîné notre stub pour les attendre. Et généralement, il n’ya pas qu’un seul test qui échoue dans de telles situations, c’est tout test qui utilise, directement ou indirectement, la méthode sendInvitations() . Nous devons corriger tous ces tests en ajoutant plus de formations. Notez également que nous ne pouvons pas supprimer les méthodes qui ne sont plus nécessaires, car nous ne soaps pas lesquelles sont inutiles. Encore une fois, cela entrave le refactoring.

De plus, la lisibilité du test a terriblement souffert, il y a beaucoup de code que nous n’avons pas écrit à cause de nous, mais parce que nous devions le faire; Ce n’est pas nous qui voulons ce code là. Les tests utilisant des objects simulés sont très complexes et sont souvent difficiles à lire. Les tests devraient aider le lecteur à comprendre comment utiliser la classe dans le cadre du test, ils devraient donc être simples et directs. S’ils ne sont pas lisibles, personne ne les entretiendra. en fait, il est plus facile de les supprimer que de les maintenir.

Comment réparer ça? Facilement:

  • Essayez d’utiliser de vraies classes au lieu de simuler autant que possible. Utilisez le vrai PdfFormatterImpl . Si ce n’est pas possible, modifiez les classes réelles pour que cela soit possible. Ne pas pouvoir utiliser une classe dans les tests indique généralement des problèmes avec la classe. Résoudre les problèmes est une situation gagnant-gagnant – vous avez corrigé la classe et vous avez un test plus simple. D’un autre côté, ne pas réparer et utiliser des simulacres est une situation sans issue – vous n’avez pas corrigé la vraie classe et vous avez des tests plus complexes et moins lisibles qui empêchent d’autres refactorings.
  • Essayez de créer une simple implémentation de test de l’interface au lieu de la simuler dans chaque test et utilisez cette classe de test dans tous vos tests. Créez TestPdfFormatter qui ne fait rien. De cette façon, vous pouvez le changer une fois pour tous les tests et vos tests ne sont pas encombrés par de longues configurations où vous entraînez vos stubs.

Globalement, les objects fictifs ont leur utilité, mais lorsqu’ils ne sont pas utilisés avec soin, ils encouragent souvent les mauvaises pratiques, testent les détails de la mise en œuvre, empêchent le refactoring et produisent des tests difficiles à lire et difficiles à maintenir .

Pour plus de détails sur les défauts des mock, voir aussi Mock Objects: Shortcomings and Use cases .

Règle générale:

Si la fonction que vous testez a besoin d’un object compliqué en tant que paramètre, et qu’il serait pénible d’instancier simplement cet object (si, par exemple, il tente d’établir une connexion TCP), utilisez un object simulé.

Vous devez simuler un object lorsque vous avez une dépendance dans une unité de code que vous essayez de tester et qui doit être “juste comme ça”.

Par exemple, lorsque vous essayez de tester une certaine logique dans votre unité de code mais que vous devez extraire quelque chose d’un autre object et que ce qui est renvoyé par cette dépendance peut affecter ce que vous essayez de tester, modélisez cet object.

Un bon podcast sur le sujet peut être trouvé ici