web-dev-qa-db-fra.com

Quand utiliser Mockito.verify ()?

J'écris des cas de test jUnit à 3 fins:

  1. Pour que mon code satisfasse à toutes les fonctionnalités requises, sous toutes (ou la plupart) des combinaisons/valeurs en entrée.
  2. Pour pouvoir modifier l’implémentation et me fier aux scénarios 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 traite, et agit comme une spécification pour le refactoring, si le code devait être réécrit. (Refacturez le code et si mes tests jUnit échouent, vous avez probablement oublié un cas d'utilisation).

Je ne comprends pas pourquoi ni quand Mockito.verify() devrait être utilisé. Quand je vois que verify() est appelé, cela signifie que mon jUnit prend conscience de la mise en œuvre. (Ainsi, changer mon implémentation endommagerait mes unités, même si mes fonctionnalités n'étaient pas affectées).

Je cherche:

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

  2. Est-il fondamentalement correct que les unités soient au courant de la mise en œuvre de la classe sous test ou soient étroitement associées à celle-ci?

189
Russell

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 créant une simulation de type C et en vérifiant que la méthode B a bien été appelée.

Cela implique que le contrat de classe A comporte suffisamment de détails pour qu'il parle du 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 va jusqu'à décrire en quelque sorte la mise en oeuvre.

Ceci est normal pour les tests unitaires. Lorsque vous testez des unités, vous voulez vous assurer que chaque unité fait ce qui est "correct", ce qui inclut généralement ses interactions avec les autres unités. "Unités" peut signifier ici des classes ou des sous-ensembles plus importants de votre application.

Mise à jour:

J’ai le sentiment que cela ne s’applique pas uniquement à la vérification, mais aussi au fait de se moquer. Dès que vous modifiez une méthode d'une classe de collaborateurs, 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. Puisque Mockito est autant une question de stratégie que de vérification, le fait que vous utilisiez 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 à changer l'inventaire des tests unitaires indiqués ici are pour la classe; à moins bien sûr que le changement ait été motivé par l’existence d’une condition que je n’ai pas testée plus tôt.

Voilà donc ce que sont les tests unitaires. Un test qui ne souffre pas de ce type de dépendance vis-à-vis de l'utilisation des classes de collaborateurs est en réalité un test de sous-système ou un test d'intégration. Bien sûr, ceux-ci sont aussi souvent écrits avec JUnit, et impliquent souvent l’utilisation de moqueurs. À mon avis, "JUnit" est un nom terrible, pour un produit qui nous permet de produire tous les types de tests.

72
Dawood ibn Kareem

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

En gros, lorsque vous testez une unité, 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érifier vous permet de le faire.

Par exemple, vous avez une partie de la logique métier censée stocker des éléments à l'aide d'un DAO. Pour ce faire, vous pouvez utiliser 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 les données attendues ont été stockées. Ce n'est plus un test unitaire.

Ou, vous pouvez vous moquer du DAO et vérifier qu'il est appelé comme vous le souhaitez. Avec mockito, vous pouvez vérifier que quelque chose est appelé, à quelle fréquence, et même utiliser des correspondants pour les paramètres afin de s’assurer qu’il est appelé de manière particulière.

Le côté opposé des tests unitaires comme celui-ci est en effet que vous liez les tests à la mise en œuvre, ce qui rend le refactoring un peu plus difficile. D'autre part, une bonne odeur de conception correspond à 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, un code avec beaucoup d'effets secondaires/interactions complexes qui doivent être testés n'est probablement pas une bonne chose à avoir.

55
Jilles van Gurp

C'est une excellente question! Je pense que la cause fondamentale en est la suivante: nous utilisons JUnit non seulement pour les tests unitaires. Donc, la question devrait être scindée:

  • Devrais-je utiliser Mockito.verify () dans mes tests intégration (ou tout autre test supérieur à l'unité)?
  • Devrais-je utiliser Mockito.verify () dans mon nité noire test unitaire?
  • Devrais-je utiliser Mockito.verify () dans mon nit à blanc tests unitaires?

donc, si nous ignorons les tests unitaires supérieurs, la question peut être reformulée " Using white-box le test unitaire avec Mockito.verify () crée un bon couple entre le test unitaire et mon pourrait implémentation, puis-je en faire quelques "boîte grise" tests unitaires et quelles règles de base je devrais utiliser pour cela ".

Maintenant, passons en revue tout cela étape par étape.

* - Dois-je utiliser Mockito.verify () dans mes tests 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 se moque de cela. Votre test doit être aussi proche que possible d'une application réelle. Vous testez un cas d'utilisation complet et non une partie isolée de l'application.

* boîte noire vs boîte blanche tests unitaires * Si vous utilisez - black-box approchez ce que vous faites réellement, vous fournissez (toutes les classes d'équivalence) en entrée, un état, et des tests pour que vous obteniez le résultat attendu. Dans cette approche, utiliser des simulacres en général est justifié (vous vous contentez de mimer qu’ils font la bonne chose; vous ne voulez pas les tester), mais appeler Mockito.verify () est superflu.

Si vous utilisez une approche zone blanche que faites-vous réellement, vous testez le comportement de votre appareil. Dans cette approche, appeler Mockito.verify () est essentiel, vous devez vérifier que votre unité se comporte comme prévu.

règles de base pour les tests de boîte grise Le problème avec les tests de boîte blanche est qu'il crée un couplage élevé. Une solution possible consiste à effectuer des tests de boîte grise et non des tests de boîte blanche. C'est en quelque sorte une combinaison de test de boîte noire et blanche. Vous testez réellement le comportement de votre unité, comme dans le test de la boîte blanche, mais en général, vous le rendez agnostique pour l'implémentation lorsque possible. Lorsque cela est possible, vous ferez simplement une vérification, comme dans le cas d'une boîte noire, en affirmant simplement que la sortie correspond à ce que vous êtes censé être. 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é précédemment avec equals () vs equalsIgnoreCase (), vous ne devez pas appeler Mockito.verify (), vous devez simplement affirmer la sortie. Si vous ne pouvez pas le faire, décomposez votre code en unités plus petites, jusqu'à ce que vous puissiez le faire. D'autre part, supposons que vous avez un @Service et que vous écrivez un @ Web-Service qui enveloppe essentiellement votre @Service - il délègue tous les appels au @Service (et effectue un traitement supplémentaire des erreurs). Dans ce cas, appeler Mockito.verify () est essentiel, vous ne devez pas dupliquer toutes les vérifications que vous avez effectuées pour @Serive, il suffit de vérifier que vous appelez @Service avec la liste de paramètres correcte.

28
alexsmail

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

  • Si vous avez d'abord créé (ou modifié) une logique métier de votre application, puis couvrez-le avec (adoptez) des tests ( Testez-Dernière approche), il sera alors très pénible et dangereux de laisser savoir aux tests le fonctionnement de votre logiciel, mis à part le contrôle des entrées et des sorties.
  • Si vous pratiquez un approche pilotée par les tests , vos tests sont les le premier à être écrit, à modifier et à refléter les cas d'utilisation de la fonctionnalité de votre logiciel. La mise en œuvre dépend de tests. Cela signifie parfois que vous souhaitez que votre logiciel soit implémenté de manière particulière, par exemple. compter sur la méthode d'un autre composant ou même l'appeler un nombre de fois particulier. C’est là que Mockito.verify () est pratique!

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

8
hammelion

Comme certaines personnes ont dit

  1. Parfois, vous ne disposez pas d'une 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 (ce dont vous vous moquez).

En ce qui concerne votre inquiétude à propos de la rupture de vos tests lors de la refactorisation, on s’attend quelque peu à l’utilisation de simulacres/talons/espions. J'entends cela par définition et non par rapport à une implémentation spécifique telle que Mockito. Mais vous pourriez penser de cette façon - si vous devez effectuer une refactorisation qui créerait des changements majeurs dans le fonctionnement de votre méthode, il est judicieux de le faire avec une approche TDD, ce qui signifie que vous pouvez modifier votre test first pour définir le nouveau comportement (qui échouera au test), et alors faire les modifications et faire passer le test à nouveau.