207 votes

Quand utiliser Mockito.verify()

J'écris des scénarios 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 compter sur les cas de test JUnit pour me dire que toutes mes fonctionnalités sont toujours satisfaites.
  3. Comme une documentation de tous les cas d'utilisation que mon code gère, et agir comme une spécification pour la refactorisation - si le code doit ê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 ou quand Mockito.verify() doit être utilisé. Quand je vois vérifier() est appelé, il me dit que mon JUnit est en train de prendre conscience de l'implémentation. (Ainsi, changer mon implémentation casserait mes jUnits, même si ma fonctionnalité n'est pas affectée).

Je suis à la recherche de :

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

  2. Est-il fondamentalement correct pour jUnits d'être conscient de, ou étroitement couplé à, l'implémentation de la classe testée ?

1 votes

J'essaie de ne pas utiliser verify() autant que possible, pour la même raison que celle que vous avez exposée (je ne veux pas que mon test unitaire prenne connaissance de l'implémentation), mais il y a un cas où je n'ai pas le choix - les méthodes void stubbées. En général, comme elles ne renvoient rien, elles ne contribuent pas à votre résultat "réel", mais vous devez quand même savoir qu'elles ont été appelées. Mais je suis d'accord avec vous, cela n'a pas de sens d'utiliser verify pour vérifier le flux d'exécution.

81voto

David Wallace Points 23911

C'est une question fantastique. +1.

Si le contrat de la classe A inclut le fait qu'elle appelle la méthode B d'un objet de type C, alors vous devez tester cela en créant un objet fantaisie de type C, et en vérifiant que la méthode B a été appelée.

Cela implique que le contrat de la classe A est suffisamment détaillé pour parler 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 simples "exigences du système" et qui va jusqu'à décrire l'implémentation.

C'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. Les "unités" peuvent être des classes ou des sous-ensembles plus importants de votre application.

Mise à jour :

J'ai l'impression que cela ne s'applique pas seulement à la vérification, mais aussi au stubbing. Dès que vous stubez 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 d'être ainsi. Puisque Mockito concerne autant le stubbing que la 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 qu'ils correspondent. Typiquement, cependant, je n'aurai pas à changer l'inventaire des tests unitaires qu'il y a sont pour la classe ; à moins, bien sûr, que la raison de ce changement soit l'existence d'une condition que je n'ai pas réussi à tester auparavant.

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

10 votes

Merci, David. Après avoir parcouru quelques 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 ne fait qu'ajouter les frais généraux de leur maintenance pour très peu de valeur. Je comprends pourquoi les mocks sont nécessaires, et pourquoi les dépendances pour l'exécution du test doivent être mises en place. Mais vérifier que la méthode dependencyA.XYZ() est exécutée rend les tests très fragiles, à mon avis.

0 votes

@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 ?

1 votes

Je ne dirais pas qu'il est totalement inutile de s'assurer qu'un sous-système ou un service a été invoqué, mais qu'il devrait y avoir des lignes directrices à ce sujet (les formuler était ce que je voulais faire). Par exemple : (je simplifie probablement trop) Disons que j'utilise StrUtil.equals() dans mon code, et que je décide de passer à StrUtil.equalsIgnoreCase() dans l'implémentation. Si jUnit avait verify(StrUtil.equals), mon test pourrait échouer bien que l'implémentation soit correcte. Cet appel à verify, IMO, est une mauvaise pratique, même s'il s'agit de bibliothèques/sous-systèmes. D'un autre côté, l'utilisation de verify pour garantir un appel à closeDbConn pourrait être un cas d'utilisation valide.

65voto

Jilles van Gurp Points 1596

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

Fondamentalement, lors d'un 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, verify vous permet de le faire.

Par exemple, vous avez une partie de la logique commerciale qui est censée stocker des choses en utilisant un DAO. Vous pouvez le faire en utilisant un test d'intégration qui instancie le DAO, le relie à la logique métier, puis fouille dans la base de données pour voir si les éléments attendus ont été stockés. Ce n'est plus un test unitaire.

Ou bien, vous pouvez simuler le DAO et vérifier qu'il est appelé de la façon dont vous l'attendez. Avec mockito vous pouvez vérifier que quelque chose est appelé, combien de fois il est appelé, et même utiliser des matchers sur les paramètres pour s'assurer qu'il est appelé d'une manière particulière.

Le revers de la médaille de ce type de tests unitaires est que vous liez les tests à l'implémentation, ce qui rend la refactorisation un peu plus difficile. D'autre part, une bonne odeur de conception est la quantité de code qu'il faut pour l'exercer correctement. Si vos tests doivent être très longs, il y a probablement un problème de conception. Ainsi, un code avec beaucoup d'effets secondaires/interactions complexes qui doivent être testés n'est probablement pas une bonne chose à avoir.

31voto

alexsmail Points 1940

C'est une excellente question ! Je pense que la cause profonde de cette question est la suivante : nous utilisons JUnit pas seulement pour les tests unitaires. La question devrait donc être divisée :

  • Devrais-je utiliser Mockito.verify() dans mon intégration (ou tout autre test supérieur aux tests unitaires) ?
  • Devrais-je utiliser Mockito.verify() dans mon boîte noire test unitaire ?
  • Devrais-je utiliser Mockito.verify() dans mon boîte blanche test unitaire ?

donc si nous ignorons les tests plus élevés que les tests unitaires, la question peut être reformulée " Utilisation de boîte blanche Le test unitaire avec Mockito.verify() crée un grand couple entre le test unitaire et mon implémentation possible, puis-je faire quelques "boîte grise" le test unitaire et quelles règles empiriques je devrais utiliser pour ceci ".

Maintenant, passons en revue tout ça étape par étape.

*- Dois-je utiliser Mockito.verify() dans ma méthode de calcul de l'impact sur l'environnement ? intégration (ou tout autre test supérieur au test unitaire) ? Je pense que la réponse est clairement non, de plus vous ne devriez pas utiliser les mocks pour cela. Votre test doit être aussi proche de l'application réelle que possible. Vous testez un cas d'utilisation complet, pas une partie isolée de l'application.

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

Si vous utilisez boîte blanche approche ce que vous faites vraiment, vous testez la comportement de votre unité. Dans cette approche, l'appel à Mockito.verify() est essentiel, vous devez vérifier que votre unité se comporte comme vous l'attendez.

règles d'or pour les tests de la boîte grise Le problème des tests en boîte blanche est qu'ils créent un couplage élevé. Une solution possible est de faire des tests en boîte grise, et non des tests en boîte blanche. Il s'agit en quelque sorte d'une combinaison de tests en boîte noire et en boîte blanche. Vous testez réellement la 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. quand c'est possible . Lorsque c'est possible, vous ferez simplement une vérification comme dans le cas de la boîte noire, en affirmant simplement que la sortie est ce que vous attendez. Donc, l'essence de votre question est quand c'est possible.

C'est vraiment difficile. Je n'ai pas de bon exemple, mais je peux vous donner quelques exemples. Dans le cas mentionné ci-dessus avec equals() vs equalsIgnoreCase(), vous ne devriez pas appeler Mockito.verify(), mais seulement asserter 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'un autre côté, supposons que vous avez un @Service et que vous écrivez un @Web-Service qui est essentiellement un wrapper sur votre @Service - il délègue tous les appels au @Service (et fait quelques traitements d'erreurs supplémentaires). Dans ce cas, l'appel à Mockito.verify() est essentiel, vous ne devez pas dupliquer toutes les vérifications que vous avez faites pour le @Serive, vérifier que vous appelez le @Service avec la bonne liste de paramètres est suffisant.

0 votes

Les tests de la boîte grise sont un peu un piège. J'ai tendance à les limiter à des choses comme les DAO. J'ai participé à des projets dont les constructions étaient extrêmement lentes en raison d'une abondance de tests boîte grise, d'un manque presque total de tests unitaires et d'un nombre bien trop important de tests boîte noire pour compenser le manque de confiance dans ce que les tests boîte grise étaient censés tester.

0 votes

Pour moi, c'est la meilleure réponse disponible puisqu'elle explique quand utiliser Mockito.when() dans une variété de situations. C'est très bien.

8voto

hammelion Points 331

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

  • Si vous avez d'abord créer (ou modifier) la logique d'entreprise de votre application et ensuite le couvrir de tests (à adopter) ( Approche test-dernier ), il sera alors très pénible et dangereux de laisser les tests connaître quoi que ce soit sur le fonctionnement de votre logiciel, si ce n'est la vérification des entrées et des sorties.
  • Si vous pratiquez une Approche fondée sur les tests , alors vos tests sont les d'abord à rédiger, à modifier et à refléter les cas d'utilisation de la fonctionnalité de votre logiciel. La mise en œuvre dépend des tests. Cela signifie parfois que vous souhaitez que votre logiciel soit mis en œuvre d'une certaine manière, par exemple en s'appuyant sur une méthode d'un autre composant ou même en l'appelant un certain nombre de fois. C'est là que le Mockito.verify() est très utile !

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 de l'équipe et bien d'autres éléments influencent la décision sur l'approche à utiliser dans votre cas particulier.

1voto

Stefan Mondelaers Points 191

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 dans celui-ci. Mais je ne pense pas que ce soit un problème. Si vous voulez être capable de changer ce que fait une méthode 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 changements. Et ce n'est pas la bonne façon de penser.

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

C'est la raison pour laquelle je préfère utiliser autant que possible des simulacres : simulez également vos objets de données. Ce faisant, vous pouvez non seulement utiliser verify pour vérifier que les méthodes correctes des autres classes sont appelées, mais aussi que les données transmises sont collectées via les méthodes correctes de ces objets de données. Et pour être complet, vous devez tester l'ordre dans lequel les appels se produisent. Exemple : si vous modifiez un objet entité de la base de données et que vous le sauvegardez ensuite à l'aide d'un référentiel, il ne suffit pas de vérifier que les fixateurs de l'objet sont appelés avec les bonnes données et que la méthode de sauvegarde du référentiel est appelée. Si elles sont appelées dans le mauvais ordre, votre méthode ne fait toujours pas ce qu'elle devrait 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 rendre complet, vous devriez aussi appeler Mockito.verifyNoMoreInteractions à la fin et lui passer tous les objets fantaisie. Sinon, quelqu'un peut ajouter une nouvelle fonctionnalité/un nouveau comportement sans le tester, ce qui signifie qu'après un certain temps, vos statistiques de couverture peuvent être de 100% et que vous empilez toujours du code qui n'est pas asserté ou vérifié.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X