Question

J'ai une compréhension de base des objets simulés et faux, mais je ne suis pas sûr de savoir quand/où utiliser la moquerie - d'autant plus que cela s'appliquerait à ce scénario. ici.

Était-ce utile?

La solution

Un test unitaire doit tester un seul chemin de code 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'effectuez pas de tests unitaires ;vous effectuez des tests d'intégration.Bien que ce soit bon et nécessaire, il ne s’agit pas de 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 nulle inattendue, et la dépendance ne peut pas générer une valeur nulle comme cela est documenté.Votre test ne rencontre pas d’exception d’argument nul comme il aurait dû le faire, et le test réussit.

En outre, vous trouverez peut-être difficile, voire impossible, d'obtenir de manière fiable que l'objet dépendant renvoie exactement ce que vous souhaitez lors d'un test.Cela inclut également la levée des exceptions attendues dans les tests.

Une simulation remplace cette dépendance.Vous définissez les attentes concernant 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 que vous puissiez tester votre code de gestion des exceptions.De cette façon, vous pouvez facilement tester l’unité en question.

TL;DR :Moquez-vous de chaque dépendance touchée par votre test unitaire.

Autres conseils

Les objets fictifs sont utiles lorsque vous souhaitez tester les interactions entre une classe testée et une interface particulière.

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

Avec des objets fictifs, au lieu de passer un vrai MailServerImpl, ou un test TestMailServer, nous pouvons adopter une implémentation simulée du MailServer interface.Avant de passer une simulation MailServer, nous le "entraînons", afin qu'il sache à quels appels de méthode s'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 bien en théorie, mais il y a aussi quelques inconvénients.

Lacunes simulées

Si vous disposez d'un framework fictif, vous êtes tenté d'utiliser un objet fictif à chaque fois vous devez transmettre une interface à la classe sous le test.De cette façon tu finis tester les interactions même lorsque cela n’est pas nécessaire.Malheureusement, les tests indésirables (accidentels) des 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 l'implémentation produise le résultat requis.

Voici un exemple en pseudocode.Supposons que nous ayons créé un MySorter classe et nous voulons le 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 souhaitons tester ;dans ce cas, ce dernier test serait effectivement valide.)

Dans un exemple aussi extrême, il est évident pourquoi ce dernier exemple est faux.Lorsque nous modifions la mise en œuvre de MySorter, le premier test fait un excellent travail en s'assurant que nous trions toujours correctement, ce qui est tout l'intérêt des tests - ils nous permettent de modifier le code en toute sécurité.En revanche, ce dernier test toujours se casse et est activement nocif ;cela entrave la refactorisation.

Se moque comme des talons

Les frameworks simulés permettent souvent également une utilisation moins stricte, dans laquelle nous n'avons pas besoin de 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 bouts.

Supposons que nous ayons une méthode sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer) que nous voulons tester.Le PdfFormatter L'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 du PdfFormatter donc nous l'entraînons simplement à accepter tranquillement n'importe quel appel et à renvoyer des valeurs de retour prédéfinies raisonnables pour toutes les méthodes qui sendInvitation() il arrive qu'il appelle à ce stade.Comment avons-nous dressé exactement cette liste de méthodes de formation ?Nous avons simplement exécuté le test et continué à ajouter les méthodes jusqu'à ce que le test réussisse.Notez 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 contents, le test passe.

Mais que se passe-t-il plus tard, quand nous changeons sendInvitations(), ou une autre classe qui sendInvitations() utilise, pour créer des PDF plus sophistiqués ?Notre test échoue soudainement parce que maintenant plus de méthodes de PdfFormatter sont appelés et nous n'avons pas entraîné notre talon à 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, le sendInvitations() méthode.Nous devons corriger tous ces tests en ajoutant davantage de formations.Notez également que nous ne pouvons pas supprimer les méthodes qui ne sont plus nécessaires, car nous ne savons pas lesquelles ne sont plus nécessaires.Encore une fois, cela gêne la refactorisation.

De plus, la lisibilité du test en a terriblement souffert, il y a beaucoup de code que nous n'avons pas écrit parce que nous le voulions, mais parce que nous le devions ;ce n'est pas nous qui voulons ce code là.Les tests qui utilisent des objets fictifs 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 conserver.

Comment résoudre ce problème ?Facilement:

  • Essayez d'utiliser de vraies classes au lieu de simulations autant que possible.Utilisez le vrai PdfFormatterImpl.Si ce n'est pas possible, changez les vraies classes 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 disposez d'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 vraie classe et vous avez des tests plus complexes et moins lisibles qui entravent les refactorisations ultérieures.
  • Essayez de créer une implémentation de test simple de l'interface au lieu de vous en moquer dans chaque test, et utilisez cette classe de test dans tous vos tests.Créer TestPdfFormatter ça ne fait rien.De cette façon, vous pouvez le modifier une fois pour tous les tests et vos tests ne sont pas encombrés de longues configurations dans lesquelles vous entraînez vos stubs.

Dans l'ensemble, les objets factices 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 la mise en œuvre, entravent la refactorisation et produisent des tests difficiles à lire et à maintenir.

Pour plus de détails sur les défauts des simulations, voir également Objets simulés :Lacunes et cas d'utilisation.

Règle générale :

Si la fonction que vous testez a besoin d'un objet compliqué comme paramètre et qu'il serait difficile d'instancier simplement cet objet (si, par exemple, il essaie d'établir une connexion TCP), utilisez un mock.

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 comme ça".

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 - moquez-vous de cet objet.

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

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top