J'ai une compréhension de base des objets simulés et faux, mais je ne suis pas sûr d'avoir une idée du moment et de l'endroit où utiliser la moquerie - d'autant plus que cela s'appliquerait à ce scénario ici .
J'ai une compréhension de base des objets simulés et faux, mais je ne suis pas sûr d'avoir une idée du moment et de l'endroit où utiliser la moquerie - d'autant plus que cela s'appliquerait à ce scénario ici .
Réponses:
Un test unitaire doit tester un seul codepath via une seule méthode. Lorsque l'exécution d'une méthode passe en dehors de cette méthode, dans un autre objet et inversement, 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 testez l'intégration. Bien que ce soit bon 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 une valeur null inattendue, et la dépendance peut ne pas lancer sur null comme il est documenté pour le faire. Votre test ne rencontre pas d'exception d'argument nul comme il se doit, et le test réussit.
En outre, vous pouvez trouver difficile, voire impossible, d'obtenir de manière fiable l'objet dépendant pour qu'il renvoie exactement ce que vous voulez pendant un test. Cela inclut également le lancement d'exceptions attendues dans les tests.
Un simulacre remplace cette dépendance. Vous définissez les attentes sur les appels à l'objet dépendant, définissez les valeurs de retour exactes qu'il doit vous donner pour effectuer le test souhaité et / ou les exceptions à lever afin de pouvoir tester votre code de gestion des exceptions. De cette manière, vous pouvez tester facilement l'unité en question.
TL; DR: simulez chaque dépendance que votre test unitaire touche.
Les objets simulés sont utiles lorsque vous souhaitez tester les interactions entre une classe testée et une interface particulière.
Par exemple, nous voulons tester ces sendInvitations(MailServer mailServer)
appels de méthode MailServer.createMessage()
exactement une fois, et également les appels MailServer.sendMessage(m)
exactement une fois, et aucune autre méthode n'est appelée sur l' MailServer
interface. C'est à ce moment que nous pouvons utiliser des objets simulés.
Avec des objets fictifs, au lieu de passer un vrai MailServerImpl
, ou un test TestMailServer
, nous pouvons passer une implémentation fictive de l' MailServer
interface. Avant de passer une simulation MailServer
, nous la «formons», de sorte qu'elle sache quelle méthode appelle à attendre et quelles valeurs de retour renvoyer. À la fin, l'objet fictif affirme que toutes les méthodes attendues ont été appelées comme prévu.
Cela semble bon en théorie, mais il y a aussi quelques inconvénients.
Si vous avez un framework fictif en place, vous êtes tenté d'utiliser un objet fictif à chaque fois que vous devez passer une interface à la classe sous le test. De cette façon, vous finissez par tester les interactions même lorsque cela n'est pas nécessaire . Malheureusement, le test indésirable (accidentel) des interactions est mauvais, car vous testez alors qu'une exigence particulière est implémentée d'une manière particulière, au lieu de cela, l'implémentation a produit le résultat requis.
Voici un exemple en pseudocode. Supposons que nous ayons créé une MySorter
classe 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 tri particulier, tel que le tri rapide, que nous voulons tester; dans ce cas, ce dernier test serait en fait valide.)
Dans un exemple aussi extrême, il est évident pourquoi ce dernier exemple est faux. Lorsque nous modifions l'implémentation de MySorter
, le premier test fait un excellent travail pour s'assurer que nous trions toujours correctement, ce qui est le but des tests - ils nous permettent de modifier le code en toute sécurité. D'un autre côté, ce dernier test se rompt toujours et il est activement nuisible; cela empêche la refactorisation.
Les frameworks fictifs permettent souvent également une utilisation moins stricte, où nous n'avons pas à spécifier exactement combien de fois les méthodes doivent être appelées et quels paramètres sont attendus; ils permettent de créer des objets fictifs qui sont utilisés comme stubs .
Supposons que nous ayons une méthode sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)
que nous voulons tester. L' PdfFormatter
objet 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' PdfFormatter
objet, donc nous l'entraînons simplement à accepter discrètement tout appel et à renvoyer des valeurs de retour prédéfinies raisonnables pour toutes les méthodes sendInvitation()
appelées à ce stade. Comment avons-nous élaboré exactement cette liste de méthodes de formation? Nous avons simplement exécuté le test et avons continué à ajouter les méthodes jusqu'à ce que le test réussisse. Remarquez que nous avons entraîné le stub à répondre à une méthode sans savoir pourquoi il doit l'appeler, nous avons simplement ajouté tout ce dont le test se plaignait. Nous sommes heureux, le test passe.
Mais que se passe-t-il plus tard, lorsque nous changeons sendInvitations()
, ou une autre classe qui sendInvitations()
utilise, pour créer des fichiers PDF plus sophistiqués? Notre test échoue soudainement car maintenant plus de méthodes de PdfFormatter
sont appelées et nous n'avons pas formé notre stub à les attendre. Et généralement, ce n'est pas seulement un test qui échoue dans des situations comme celle-ci, c'est tout test qui utilise, directement ou indirectement, la sendInvitations()
méthode. Nous devons corriger tous ces tests en ajoutant plus de formations. Notez également que nous ne pouvons pas supprimer les méthodes dont nous n'avons plus besoin, car nous ne savons pas lesquelles d'entre elles ne sont pas nécessaires. Encore une fois, cela empêche la refactorisation.
De plus, la lisibilité du test a terriblement souffert, il y a beaucoup de code là-bas que nous n'avons pas écrit parce que nous le voulions, mais parce que nous devions le faire; ce n'est pas nous qui voulons ce code là-bas. Les tests qui utilisent des objets factices semblent très complexes et sont souvent difficiles à lire. Les tests doivent aider le lecteur à comprendre comment la classe soumise au test doit être utilisée, ils doivent donc être simples et directs. S'ils ne sont pas lisibles, personne ne les maintiendra; en fait, il est plus facile de les supprimer que de les maintenir.
Comment y remédier? Facilement:
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. La résolution des 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 le réparer et utiliser des simulations est une situation sans issue - vous n'avez pas corrigé la classe réelle et vous avez des tests plus complexes et moins lisibles qui empêchent les refactorisations ultérieures.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 de longues configurations où vous formez vos stubs.Dans l'ensemble, les objets simulés ont leur utilité, mais lorsqu'ils ne sont pas utilisés avec précaution, ils encouragent souvent les mauvaises pratiques, testent les détails de mise en œuvre, entravent la refactorisation et produisent des tests difficiles à lire et à maintenir .
Pour plus de détails sur les lacunes des simulacres, consultez également Objets simulés: lacunes et cas d'utilisation .
Règle de base:
Si la fonction que vous testez a besoin d'un objet compliqué en tant que paramètre et qu'il serait pénible de simplement instancier cet objet (si, par exemple, elle essaie d'établir une connexion TCP), utilisez une simulation.
Vous devez vous moquer d'un objet lorsque vous avez une dépendance dans une unité de code que vous essayez de tester et qui doit être "juste ainsi".
Par exemple, lorsque vous essayez de tester une logique dans votre unité de code mais que vous devez obtenir quelque chose d'un autre objet et que ce qui est renvoyé par cette dépendance peut affecter ce que vous essayez de tester - simulez cet objet.
Un excellent podcast sur le sujet peut être trouvé ici