Quand utiliser Mockito.verify ()?

201

J'écris des cas de test jUnit pour 3 raisons:

  1. Pour m'assurer que mon code satisfait toutes les fonctionnalités requises, sous toutes (ou la plupart) des combinaisons / valeurs d'entrée.
  2. Pour m'assurer que je peux modifier l'implémentation et me fier aux cas de test JUnit pour me dire que toutes mes fonctionnalités sont toujours satisfaites
  3. En tant que documentation de tous les cas d'utilisation que mon code gère, et sert de spécification pour la refactorisation - si le code devait jamais être réécrit. (Refactorisez le code, et si mes tests jUnit échouent - vous avez probablement manqué un cas d'utilisation).

Je ne comprends pas pourquoi ni quand Mockito.verify()doit être utilisé. Quand je vois verify()être appelé, cela me dit que mon jUnit prend conscience de l'implémentation. (Ainsi, changer mon implémentation casserait mes jUnits, même si ma fonctionnalité n'était pas affectée).

Je cherche:

  1. Quelles devraient être les directives pour une utilisation appropriée de Mockito.verify()?

  2. Est-il fondamentalement correct que jUnits soit au courant ou étroitement lié à la mise en œuvre de la classe testée?

Russell
la source
1
J'essaie de ne pas utiliser Verify () autant que possible, pour la même raison que vous avez exposée (je ne veux pas que mon test unitaire soit au courant de la mise en œuvre), mais il y a un cas où je n'ai pas le choix - méthodes vides tronquées. D'une manière générale, car ils ne renvoient rien, ils ne contribuent pas à votre sortie «réelle»; mais encore faut-il savoir qu'elle a été appelée. Mais je suis d'accord avec vous, cela n'a aucun sens d'utiliser la vérification pour vérifier le flux d'exécution.
Legna

Réponses:

78

Si le contrat de classe A inclut le fait qu'il appelle la méthode B d'un objet de type C, vous devez le tester en faisant une maquette de type C et en vérifiant que la méthode B a été appelée.

Cela implique que le contrat de classe A a suffisamment de détails pour qu'il parle de type C (qui peut être une interface ou une classe). Donc, oui, nous parlons d'un niveau de spécification qui va au-delà des «exigences système» et décrit en quelque sorte l'implémentation.

Ceci est normal pour les tests unitaires. Lorsque vous effectuez des tests unitaires, vous voulez vous assurer que chaque unité fait la "bonne chose", et cela inclut généralement ses interactions avec les autres unités. «Unités» ici peut signifier des classes ou des sous-ensembles plus importants de votre application.

Mettre à jour:

Je pense que cela ne s'applique pas seulement à la vérification, mais aussi au stubbing. Dès que vous écrivez une méthode d'une classe de collaborateur, votre test unitaire est devenu, dans un certain sens, dépendant de l'implémentation. C'est un peu dans la nature des tests unitaires de l'être. Étant donné que Mockito concerne autant le stubbing que la vérification, le fait que vous utilisez Mockito implique que vous allez rencontrer ce type de dépendance.

D'après mon expérience, si je change l'implémentation d'une classe, je dois souvent changer l'implémentation de ses tests unitaires pour correspondre. En règle générale, cependant, je n'aurai pas à modifier l'inventaire des tests unitaires qu'il y a pour la classe; sauf bien sûr, la raison du changement était l'existence d'une condition que je n'ai pas testée plus tôt.

Voilà donc à quoi servent les tests unitaires. Un test qui ne souffre pas de ce type de dépendance sur la façon dont les classes collaboratrices sont utilisées est en réalité un test de sous-système ou un test d'intégration. Bien sûr, ceux-ci sont également fréquemment écrits avec JUnit et impliquent fréquemment l'utilisation de la moquerie. À mon avis, "JUnit" est un nom terrible, pour un produit qui nous permet de produire tous les différents types de tests.

Dawood ibn Kareem
la source
8
Merci, David. Après avoir parcouru certains ensembles de codes, cela semble être une pratique courante - mais pour moi, cela va à l'encontre de l'objectif de créer des tests unitaires, et ajoute juste la surcharge de les maintenir pour très peu de valeur. Je comprends pourquoi des simulations sont requises et pourquoi les dépendances pour l'exécution du test doivent être configurées. Mais vérifier que la méthode dependencyA.XYZ () est exécutée rend les tests très fragiles, à mon avis.
Russell
@Russell Même si "type C" est une interface pour un wrapper autour d'une bibliothèque, ou autour d'un sous-système distinct de votre application?
Dawood ibn Kareem
1
Je ne dirais pas qu'il est complètement inutile de s'assurer qu'un sous-système ou un service a été invoqué - juste qu'il devrait y avoir des directives à ce sujet (les formuler était ce que je voulais faire). Par exemple: (je suis probablement en train de trop simplifier) ​​Dis, j'utilise StrUtil.equals () dans mon code, et décide de basculer vers StrUtil.equalsIgnoreCase () dans l'implémentation.Si jUnit avait vérifier (StrUtil.equals ), mon test peut échouer même si la mise en œuvre est précise. Cet appel de vérification, IMO, est une mauvaise pratique bien qu'il concerne les bibliothèques / sous-systèmes. D'un autre côté, l'utilisation de Verify pour garantir qu'un appel à closeDbConn peut être une cas d'utilisation valide.
Russell
1
Je vous comprends et suis entièrement d'accord avec vous. Mais je pense également que la rédaction des lignes directrices que vous décrivez pourrait s'étendre à la rédaction d'un manuel TDD ou BDD complet. Pour prendre votre exemple, appeler equals()ou equalsIgnoreCase()ne serait jamais quelque chose qui était spécifié dans les exigences d'une classe, n'aurait donc jamais de test unitaire en soi. Cependant, "fermer la connexion DB une fois terminée" (quoi que cela signifie en termes d'implémentation) peut bien être une exigence d'une classe, même si ce n'est pas une "exigence métier". Pour moi, cela se résume à la relation entre le contrat ...
Dawood ibn Kareem
... d'une classe telle qu'exprimée dans ses exigences commerciales, et l'ensemble des méthodes de test qui testent à l'unité cette classe. La définition de cette relation serait un sujet important dans tout livre sur TDD ou BDD. Alors que quelqu'un de l'équipe Mockito pourrait écrire un article sur ce sujet pour leur wiki, je ne vois pas en quoi cela différerait de beaucoup d'autres publications disponibles. Si vous voyez en quoi cela peut différer, faites-le moi savoir et peut-être pourrons-nous y travailler ensemble.
Dawood ibn Kareem
60

La réponse de David est bien sûr correcte, mais n'explique pas très bien pourquoi vous voudriez cela.

Fondamentalement, lors du test unitaire, vous testez une unité de fonctionnalité de manière isolée. Vous testez si l'entrée produit la sortie attendue. Parfois, vous devez également tester les effets secondaires. En un mot, vérifiez vous permet de le faire.

Par exemple, vous avez un peu de logique métier qui est censé stocker des choses à l'aide d'un DAO. Vous pouvez le faire en utilisant un test d'intégration qui instancie le DAO, le connecte à la logique métier, puis fouille dans la base de données pour voir si le contenu attendu a été stocké. Ce n'est plus un test unitaire.

Ou, vous pouvez vous moquer du DAO et vérifier qu'il est appelé de la manière attendue. Avec mockito, vous pouvez vérifier que quelque chose est appelé, à quelle fréquence il est appelé, et même utiliser des égaliseurs sur les paramètres pour vous assurer qu'il est appelé d'une manière particulière.

L'inconvénient des tests unitaires comme celui-ci est en effet que vous liez les tests à la mise en œuvre, ce qui rend la refactorisation un peu plus difficile. D'un autre côté, une bonne odeur de conception est la quantité de code nécessaire pour l'exercer correctement. Si vos tests doivent être très longs, il y a probablement un problème avec la conception. Donc, le code avec beaucoup d'effets secondaires / interactions complexes qui doivent être testés n'est probablement pas une bonne chose.

Jilles van Gurp
la source
30

C'est une excellente question! Je pense que la cause première est la suivante, nous utilisons JUnit non seulement pour les tests unitaires. La question devrait donc être divisée:

  • Dois-je utiliser Mockito.verify () dans mes tests d' intégration (ou tout autre test supérieur à l'unité)?
  • Dois-je utiliser Mockito.verify () dans mes tests unitaires de boîte noire ?
  • Dois-je utiliser Mockito.verify () dans mes tests unitaires en boîte blanche ?

Donc, si nous ignorons les tests supérieurs à l'unité, la question peut être reformulée "L' utilisation de tests unitaires en boîte blanche avec Mockito.verify () crée un grand couple entre le test unitaire et mon implémentation pourrait, puis-je en faire " une boîte grise msgstr " tests unitaires et règles générales à utiliser pour cela ".

Passons maintenant en revue toutes ces étapes.

* - Dois-je utiliser Mockito.verify () dans mes tests d' intégration (ou tout autre test supérieur à l'unité)? * Je pense que la réponse est clairement non, de plus vous ne devriez pas utiliser de maquette pour cela. Votre test doit être aussi proche que possible de l'application réelle. Vous testez un cas d'utilisation complet, pas une partie isolée de l'application.

* tests unitaires boîte noire vs boîte blanche * Si vous utilisez l' approche boîte noire , que faites-vous réellement, vous fournissez une entrée (toutes les classes d'équivalence), un état et des tests qui vous permettront d'obtenir la sortie attendue. Dans cette approche, l'utilisation de simulacres en général est justifiée (vous imitez simplement qu'ils font la bonne chose; vous ne voulez pas les tester), mais appeler Mockito.verify () est superflu.

Si vous utilisez l' approche en boîte blanche de ce que vous faites réellement, vous testez le comportement de votre unité. Dans cette approche, appeler Mockito.verify () est essentiel, vous devez vérifier que votre unité se comporte comme prévu.

règles générales pour les tests en boîte grise Le problème avec les tests en boîte blanche est qu'il crée un couplage élevé. Une solution possible est de faire des tests en boîte grise, pas des tests en boîte blanche. Il s'agit en quelque sorte d'une combinaison de tests en boîte noire et blanche. Vous testez vraiment le comportement de votre unité comme dans les tests en boîte blanche, mais en général vous le rendez indépendant de l'implémentation lorsque cela est possible . Lorsque cela est possible, vous effectuerez simplement une vérification comme dans le cas de la boîte noire, affirmant simplement que la sortie est ce que vous attendez. Donc, l'essence de votre question est de savoir quand c'est possible.

C'est vraiment difficile. Je n'ai pas de bon exemple, mais je peux vous donner des exemples. Dans le cas mentionné ci-dessus avec equals () vs equalsIgnoreCase (), vous ne devez pas appeler Mockito.verify (), il suffit d'affirmer la sortie. Si vous ne pouvez pas le faire, décomposez votre code en une unité plus petite, jusqu'à ce que vous puissiez le faire. D'un autre côté, supposons que vous ayez un @Service et que vous écrivez @ Web-Service qui est essentiellement un wrapper sur votre @Service - il délègue tous les appels au @Service (et fait une gestion supplémentaire des erreurs). Dans ce cas, appeler à Mockito.verify () est essentiel, vous ne devez pas dupliquer toutes vos vérifications que vous avez faites pour @Serive, vérifier que vous appelez à @Service avec la liste de paramètres correcte est suffisant.

alexsmail
la source
Les tests en boîte grise sont un peu un écueil. J'ai tendance à le limiter à des choses comme les DAO. J'ai été sur certains projets avec des constructions extrêmement lentes en raison d'une abondance de tests de boîte grise, d'un manque presque complet de tests unitaires et de trop de tests de boîte noire pour compenser le manque de confiance dans ce que les tests de grise étaient censés tester.
Jilles van Gurp
Pour moi, c'est la meilleure réponse disponible car elle indique quand utiliser Mockito.when () dans diverses situations. Bien joué.
Michiel Leegwater
8

Je dois dire que vous avez absolument raison du point de vue d'une approche classique:

  • Si vous créez (ou modifiez) la logique métier de votre application, puis la recouvrez de (adoptez) des tests ( approche Test-Last ), il sera très pénible et dangereux de faire savoir aux tests quoi que ce soit sur le fonctionnement de votre logiciel, autre que vérification des entrées et sorties.
  • Si vous pratiquez une approche pilotée par les tests, vos tests sont les premiers à être écrits, à être modifiés et à refléter les cas d'utilisation des fonctionnalités de votre logiciel. L'implémentation dépend de tests. Cela signifie parfois que vous voulez que votre logiciel soit implémenté d'une manière particulière, par exemple en s'appuyant sur la méthode d'un autre composant ou même en l'appelant un nombre de fois particulier. C'est là que Mockito.verify () est utile!

Il est important de se rappeler qu'il n'y a pas d'outils universels. Le type de logiciel, sa taille, les objectifs de l'entreprise et la situation du marché, les compétences de l'équipe et bien d'autres choses influencent la décision sur l'approche à utiliser dans votre cas particulier.

hammelion
la source
0

Comme certains l'ont dit

  1. Parfois, vous n'avez pas de sortie directe sur laquelle vous pouvez affirmer
  2. Parfois, il vous suffit de confirmer que votre méthode testée envoie les bons résultats indirects à ses collaborateurs (dont vous vous moquez).

En ce qui concerne votre souci de casser vos tests lors de la refactorisation, cela est quelque peu attendu lors de l'utilisation de simulacres / talons / espions. Je veux dire que par définition et non en ce qui concerne une mise en œuvre spécifique telle que Mockito. Mais vous pourriez penser de cette façon - si vous avez besoin de faire un refactoring qui créerait des changements majeurs sur la façon dont votre méthode fonctionne, il est une bonne idée de le faire sur une approche TDD, ce qui signifie que vous pouvez modifier votre test d' abord pour définir la nouveau comportement (qui échouera au test), puis effectuez les modifications et réussissez à nouveau le test.

Emanuel Luiz Lariguet Beltrame
la source
0

Dans la plupart des cas, lorsque les gens n'aiment pas utiliser Mockito.verify, c'est parce qu'il est utilisé pour vérifier tout ce que l'unité testée fait et cela signifie que vous devrez adapter votre test si quelque chose change. Mais je ne pense pas que ce soit un problème. Si vous voulez pouvoir changer ce qu'une méthode fait sans avoir besoin de changer son test, cela signifie essentiellement que vous voulez écrire des tests qui ne testent pas tout ce que fait votre méthode, parce que vous ne voulez pas qu'elle teste vos modifications . Et c'est la mauvaise façon de penser.

Ce qui est vraiment un problème, c'est si vous pouvez modifier ce que fait votre méthode et qu'un test unitaire censé couvrir entièrement la fonctionnalité n'échoue pas. Cela signifierait que quelle que soit l'intention de votre changement, le résultat de votre changement n'est pas couvert par le test.

Pour cette raison, je préfère me moquer autant que possible: moquer aussi vos objets de données. En faisant cela, vous pouvez non seulement utiliser la vérification pour vérifier que les méthodes correctes des autres classes sont appelées, mais également que les données transmises sont collectées via les méthodes correctes de ces objets de données. Et pour le terminer, vous devez tester l'ordre dans lequel les appels se produisent. Exemple: si vous modifiez un objet entité db puis l'enregistrez à l'aide d'un référentiel, il ne suffit pas de vérifier que les setters de l'objet sont appelés avec les données correctes et que la méthode de sauvegarde du référentiel est appelée. S'ils sont appelés dans le mauvais ordre, votre méthode ne fait toujours pas ce qu'elle doit faire. Donc, je n'utilise pas Mockito.verify mais je crée un objet inOrder avec tous les mocks et j'utilise inOrder.verify à la place. Et si vous voulez le terminer, vous devez également appeler Mockito. vérifierNoMoreInteractions à la fin et lui passer toutes les moqueries. Sinon, quelqu'un peut ajouter de nouvelles fonctionnalités / comportements sans les tester, ce qui signifierait après que vos statistiques de couverture peuvent être à 100% et que vous empilez du code qui n'est pas affirmé ou vérifié.

Stefan Mondelaers
la source