Comment fonctionne l'invocation de mockito when ()?

111

Compte tenu de la déclaration Mockito suivante:

when(mock.method()).thenReturn(someValue);

Comment Mockito crée-t-il un proxy pour un simulacre, étant donné que l'instruction mock.method () passera la valeur de retour à when ()? J'imagine que cela utilise des trucs CGLib, mais je serais intéressé de savoir comment cela est techniquement fait.

marchaos
la source

Réponses:

118

La réponse courte est que dans votre exemple, le résultat de mock.method()sera une valeur vide appropriée au type; mockito utilise l'indirection via le proxy, l'interception de méthode et une instance partagée de la MockingProgressclasse afin de déterminer si une invocation d'une méthode sur une maquette est pour le stubbing ou la relecture d'un comportement stubbed existant plutôt que de transmettre des informations sur le stubbing via la valeur de retour de une méthode moquée.

Une mini-analyse en quelques minutes en regardant le code mockito est la suivante. Notez qu'il s'agit d'une description très approximative - il y a beaucoup de détails en jeu ici. Je vous suggère de vérifier vous-même la source sur github .

Tout d'abord, lorsque vous vous moquez d'une classe en utilisant la mockméthode de la Mockitoclasse, c'est essentiellement ce qui se passe:

  1. Mockito.mockdélègue à org.mockito.internal.MockitoCore.mock, en passant les paramètres de simulation par défaut en tant que paramètre.
  2. MockitoCore.mockdélègue à org.mockito.internal.util.MockUtil.createMock
  3. La MockUtilclasse utilise la ClassPathLoaderclasse pour obtenir une instance de MockMakerà utiliser pour créer la maquette. Par défaut, la classe CgLibMockMaker est utilisée.
  4. CgLibMockMakerutilise une classe empruntée à JMock, ClassImposterizerqui gère la création de la maquette. Les éléments clés de la «magie mockito» utilisés sont ceux MethodInterceptorutilisés pour créer le simulacre: le mockito MethodInterceptorFilter, et une chaîne d'instances MockHandler, y compris une instance de MockHandlerImpl . L'intercepteur de méthode passe les appels à l'instance MockHandlerImpl, qui implémente la logique métier à appliquer lorsqu'une méthode est invoquée sur un simulacre (c'est-à-dire, en cherchant à voir si une réponse est déjà enregistrée, en déterminant si l'appel représente un nouveau stub, etc. L'état par défaut est que si un stub n'est pas déjà inscrit pour la méthode appelée, une valeur vide appropriée au type est renvoyée.

Maintenant, regardons le code dans votre exemple:

when(mock.method()).thenReturn(someValue)

Voici l'ordre dans lequel ce code s'exécutera:

  1. mock.method()
  2. when(<result of step 1>)
  3. <result of step 2>.thenReturn

La clé pour comprendre ce qui se passe est ce qui se passe lorsque la méthode sur la maquette est appelée: l'intercepteur de méthode reçoit des informations sur l'appel de méthode et les délègue à sa chaîne d' MockHandlerinstances, qui finit par déléguer à MockHandlerImpl#handle. Pendant MockHandlerImpl#handle, le gestionnaire de simulation crée une instance de OngoingStubbingImplet la transmet à l' MockingProgressinstance partagée .

Lorsque la whenméthode est appelée après l'appel de method(), elle délègue à MockitoCore.when, qui appelle la stub()méthode de la même classe. Cette méthode décompresse le stubbing en cours de l' MockingProgressinstance partagée dans laquelle l' method()appel simulé a écrit et la renvoie. Ensuite, la thenReturnméthode est appelée sur l' OngoingStubbinginstance.

Paul Morie
la source
1
Merci pour la réponse détaillée. Une autre question - vous mentionnez que "lorsque la méthode est invoquée après l'invocation de method ()" - comment sait-il que l'invocation de when () est la toute prochaine invocation (ou encapsule) l'invocation de method ()? J'espère que cela a du sens.
marchaos
@marchaos Il ne sait pas. Avec la when(mock.method()).thenXyz(...)syntaxe, mock.method()s'exécute en mode "replay", pas en mode "stubbing". Normalement, cette exécution de mock.method()n'a aucun effet, donc plus tard quand thenXyz(...)( thenReturn, thenThrow, thenAnswer, etc.) est exécuté, il passe en mode « stubbing » puis enregistre le résultat souhaité pour cet appel de méthode.
Rogério
1
Rogerio, c'est en fait un peu plus subtil que cela - mockito n'a pas de modes de stubbing et de relecture explicites. Je modifierai ma réponse plus tard pour qu'elle soit plus claire.
Paul Morie
Je résumerais qu'en bref, il est plus facile d'intercepter un appel de méthode dans une autre méthode avec CGLIB ou Javassist que d'intercepter, disons, l'opérateur "if".
Infeligo
Je n'ai pas encore massé ma description ici, mais je ne l'ai pas oublié non plus. FYI.
Paul Morie
33

La réponse courte est, dans les coulisses, Mockito utilise une sorte de variables globales / stockage pour enregistrer les informations des étapes de construction de stub de méthode (invocation de method (), when (), thenReturn () dans votre exemple), de sorte qu'il puisse éventuellement construire une carte sur ce qui doit être retourné quand ce qui est appelé sur quel param.

J'ai trouvé cet article très utile: Explication du fonctionnement des Mock Frameworks basés sur proxy ( http://blog.rseiler.at/2014/06/explanation-how-proxy-based-mock.html ). L'auteur a implémenté un framework de démonstration Mocking, que j'ai trouvé une très bonne ressource pour les personnes qui veulent comprendre comment ces frameworks Mocking fonctionnent.

À mon avis, c'est une utilisation typique d'Anti-Pattern. Normalement, nous devrions éviter les «effets secondaires» lorsque nous implémentons une méthode, ce qui signifie que la méthode doit accepter l'entrée et faire un calcul et retourner le résultat - rien d'autre n'a changé à part cela. Mais Mockito viole cette règle exprès. Ses méthodes stockent un tas d'informations en plus de renvoyer le résultat: Mockito.anyString (), mockInstance.method (), when (), thenReturn, elles ont toutes un «effet secondaire» spécial. C'est aussi pourquoi le framework ressemble à une magie à première vue - nous n'écrivons généralement pas de code comme ça. Cependant, dans le cas du framework moqueur, cette conception anti-modèle est une excellente conception, car elle conduit à une API très simple.

Jacob Wu
la source
4
Excellent lien. Le génie derrière cela est: L'API très simple qui rend tout cela très agréable. Une autre bonne décision est que la méthode when () utilise des génériques afin que la méthode thenReturn () soit de type sécurisé.
David Tonhofer
Je considère que c'est la meilleure réponse. Contrairement à l'autre réponse, elle explique clairement les concepts du processus de moquerie au lieu du flux de contrôle à travers du code concret. Je suis d'accord, excellent lien.
mihca