"We decided during the main conference that we should use JUnit 4 and Mockito because we think they are the future of TDD and mocking in Java" - Dan North, créateur du terme BDD
La syntaxe élégante de Mockito explique en grande partie son succès. Comment Mockito réussit-il à garder nos tests lisibles ? Que se cache-t-il derrière cette API ? C’est ce que nous allons découvrir.
Mockito est publié sous licence MIT. Le code présenté dans cet article a été simplifié pour des raisons évidentes et n’a pas pour vocation à être utilisé en dehors de ce contexte d’apprentissage. Cet article est basé sur la dernière version de Mockito (1.10.14) au moment de cet article.
Un exemple
Cette interface très basique reprend le principe d’une registry. Pour limiter les appels, on décide de mettre en place un simple cache :
Comment tester cette dernière classe et garantir que la registry n’est pas appelée deux fois pour un même nom ? L’utilisation d’un Test Double de type mock est nécessaire (Voir ce post de Martin Fowler pour les différences entre les différents types de Test Double).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import static org.mockito.Mockito.*;
public class RegistryCacheDecoratorUnitTest
{
private RegistryCacheDecorator decorator;
private Registry registry;
@Before
public void before() {
registry = mock(Registry.class);
decorator = new RegistryCacheDecorator(registry);
}
@Test
public void registryShouldOnlyBeCalledOnceForTheSameName() {
// Given
when(registry.lookup(anyString())).thenReturn(new BasicDataSource());
// When
decorator.lookup("datasource");
decorator.lookup("datasource");
// Then
verify(registry, times(1)).lookup("datasource");
}
}
- 11
- On instancie un nouveau mock pour chaque test
- 19
- On configure ce mock pour associer une instance de
DataSource
au nom"dataSource"
- 26
- On vérifie que la registry n’a été sollicitée qu’une seule fois avec ce nom.
Au lancement, le test passe au vert. L’objectif est maintenant de supprimer les imports Mockito et de réécrire le minimum de code pour faire fonctionner de nouveau ce test. On veillera à être le plus fidèle possible à l’implémentation de Mockito, dans le nommage des classes, dans les algorithmes, etc, même si quelques libertés seront prises pour ne pas trop allonger cet article.
C’est parti !
Suite à la suppression des imports, notre code ne compile plus. On commence donc par définir la signature des méthodes manquantes :
Notre code ne compile toujours pas à cause de la référence à OngoingStubbing
.
Le code suivant devrait corriger le problème :
Vous pouvez pour le moment ignorer cette interface, nous y reviendrons en temps voulu.
mock
: La création d’un mock
Créer un mock revient à créer un proxy, c’est-à-dire un objet qui se présente comme n’importe quel instance d’une
classe mais dont le comportement est préprogrammé. Depuis Java 1.3, on peut utiliser les
Dynamic Proxies pour créer
dynamiquement une classe mais seulement à partir d’une interface. Cette restriction est problématique en pratique
et c’est pour cette raison que la plupart des librairies comme Spring, Hibernate ou encore Mockito utilisent une
librairie qui manipule directement le bytecode. La plus célèbre est
cglib. Ces librairies fonctionnent en créant une
sous-classe de la classe donnée, ce qui s’avère bien plus souple (à condition que la classe ne soit pas final
).
Bien que reposant sur cglib, Mockito cherche à s’en abstraire à l’aide deux interfaces : MockMaker
et MockHandler
.
Une implémentation de MockMaker
est responsable d’instancier un proxy de manière à ce que chaque
appel de méthodes soit délégué à la classe MockHandler
:
La classe Invocation
est quant à elle un simple Wrapper regroupant les propriétés liées à l’appel :
cglib/ASM/Objenesis : le trio gagnant
On touche du doigt la partie la plus bas niveau de Mockito. Le code reste néanmoins facilement compréhensible
grâce à l’API de cglib qui repose sur celle encore plus bas niveau d’ASM.
Voici l’implémentation de MockMaker
:
Pas de panible. Le code est bien moins obscur qu’il n’y parait. Déroulons le code pas à pas.
- On commence par créer une instance
de Enhancer
,
la classe principale de cglib, chargée de créer de nouvelles classes dynamiquement.
- On décrit ensuite ce que l’on cherche à obtenir :
La ligne plus importante est la seconde où l’on spécifie la classe de notre mock. Pour comprendre la première
ligne, il faut savoir que toutes les classes générées par cglib implémente par défaut l’interface
Factory
. Cette interface permet par exemple de changer de callback (patience nous y arrivons).
La méthode setUseFactory
permet de désactiver cela mais notre ligne ne fait que confirmer le défaut et
est donc inutile. La dernière ligne indique le type de callback que l’on va utiliser. Plusieurs sont disponibles
comme FixedValue
qui retourne une valeur fixe à chaque appel de méthode. Le callback le plus souple
est MethodInterceptor
qui nous donne accès à toutes les informations concernant l’appel de méthode.
- Reste alors à créer notre classe dynamique qui va nous servir de modèle pour instancier notre mock.
- Le moyen le plus simple pour créer une instance à partir d’un objet Class
est la méthode newInstance
:
Cette méthode s’appuie sur un constructeur par défaut. Cette restriction généralement acceptable peut poser quelques problèmes dans notre cas.
Imaginons que la classe à mocker hérite d’une autre classe :
Suivant le code du constructeur parent, le résultat peut être problématique.
Peut-on instancier un objet en Java sans utiliser de constructeur ?
La réponse peut surprendre mais oui, grâce à la librairie Objenesis. Là encore, il s’agit de manipulation de bytecode qui diffère selon la version de JVM, le vendeur et la version de la JVM du vendeur... (voir classe StdInstantiatorStrategy pour les plus curieux).
Avec ces nouvelles connaissances, nous pouvons revenir à notre MockHandler
:
Objenesis crée une nouvelle instance de notre classe dynamique. Notre mock vient enfin de naître.
On lui associe une instance de MethodInterceptorFilter
, pour faire le lien entre cglib et notre
MockHandler
.
Avant de clore cette première partie, certains auront peut-être remarqué que Mockito repackage cglib (et ASM) :
Pourquoi repackager cglib ?
Cglib n’offre pas une maintenance à l’image de sa popularité. A cela s’ajoute quelques versions instables qui ont atteri dans le Central Maven qui forcent donc des librairies comme Mockito ou Spring à repackager la librairie dans leur propre namespace pour garantir une version stable.
Cela soulève une autre question
cglib reste-il toujours la solution incontournable ?
La tendance est clairement non. Les frameworks majeurs comme Hibernate ou Spring ont ou envisage une migration vers une autre solution comme javassist.
Pour en savoir plus sur CGlib, un excellent article est disponible pour combler, soyons honnête, l’absence totale d’une documentation officielle.
when
: programmation du mock
Même si nous en avons fini avec le code bas niveau, la suite n’en est pas plus simple. L’API tellement bien pensée cache en réalité beaucoup d’ingéniosité pour la rendre opérationnelle.
Un premier aperçu....
Lors de l’exécution de cette ligne :
-
la méthode
anyString
est d’abord appelée. On mémorise l’utilisation de cetArgumentMatcher
(dans une variable globale). -
la méthode
registry#lookup(String)
est ensuite véritablement appelée (celle de notre mock). On mémorise l’invocation toujours de manière globale. -
la méthode
when
est appelée. On sait seulement alors que nous sommes en train de configurer notre mock. -
la méthode
thenReturn
est appelée. On exploite les données précédemment collectées, on sauvegarde la réponse pour être ensuite retournée quand le mock sera exercé pendant le test.
Commençons par nous pencher sur cette fameuse variable globale, véritable boussole pour savoir à tout moment
où nous nous trouvons. Cette variable est en réalité une instance de la classe MockingProgress
:
Cette classe sera modifiée lorsque nous attaquerons la dernière partie. Son code peut dérouter mais son principe est assez simple : dès qu’on en sait plus sur notre position dans le code, on le communique à cette classe qui permet également de retrouver au moment voulu les informations sauvegardées.
Son fonctionnement deviendra plus clair à travers les prochaines classes.
Revenons à quelque chose de plus simple pour le moment : les ArgumentMatcher
, basés sur l’excellente
librairie Hamcrest :
Mockito propose de nombreux matchers. Pour notre exemple, seuls deux seront nécessaires :
Leur utilisation passe forcément par une méthode factory qui a double emploi : communiquer leur usage et
retourner le type adapté pour que notre code compile (Note : instancier directement un matcher à la place du
anyString()
ne compilerait pas) :
Avant de définitivement passer aux choses sérieuses, nous allons introduire la classe
InvocationMatcher
que l’on va retrouver à plusieurs reprises. Cette classe regroupe à la fois une
Invocation
(un appel de méthode) avec la liste des matchers utilisés. Même si nous disposons des
arguments dans l’objet Invocation
, les matchers eux ne sont pas présents comme en atteste la classe
Matchers
que nous venons de voir (anyString
retourne par exemple une chaine vide).
Voici le code de cette classe :
- 16
-
Lors d’un appel de méthode sur notre mock (aussi bien pour le
when
ou que pour leverify
), Mockito autorise soit uniquement des littéraux/variables, ou uniquement des matchers. Par facilité d’implémentation, Mockito veille à ne travailler qu’avec des matchers. C’est le rôle de la méthodeargumentsToMatchers
.
Il nous reste encore plusieurs classes à aborder. Continuons avec la classe InvocationContainer
.
Contrairement à MockingProgress
qui est partagée entre tous les mocks et tous les tests, chaque mock
dispose de sa propre instance de InvocationContainer
. Cette classe conserve l’ensemble des
invocations "stubbées", c’est-à-dire les invocations à l’aide du when
permettant de programmer notre
mock mais aussi l’ensemble des invocations réelles durant l’exécution du test, celles qui vont servir à valider
nos affirmations (verify
).
- 4
-
stubbed
contient tous les appels stubbés enregistrés à l'aide duwhen
. - 5
-
invocationForStubbing
contient l'appel en cours. On ne sait pas encore si il s'agit d'un appel stubbé (when
), d'un appel normal, ou d'un appel dans le cadre duverify
. - 6
-
registeredInvocations
contient tous les appels réels aux mocks, c'est-à-dire les appels qui n'interviennent pas lors d'unwhen
ouverify
. - 8
-
Cette méthode est appelée à chaque appel d’une méthode sur un mock. Il peut s’agir d’un appel durant un
when
ou d’un appel réel. Dans le premier cas, la méthodeaddAnwser
sera ensuite appelée (par lethenReturn
par exemple) pour nous permettre d’ajouter cette invocation dans la liste des invocations "stubbées". Pour le second cas, nous n’avons pas d’équivalent. Il s’agit là de notre seule chance de prendre note de l’appel. On ajoute donc l’invocation à la liste des invocations réelles et on la supprimera si jamais la méthodeaddAnswer
est appelée.
Les réponses sont représentées par l’interface Answer
. Elles peuvent correspondre à une valeur de
retour (thenReturn
), à une exception à lancer (thenThrow
), etc.
L’enregistrement des réponses se fait par une des premières classes introduites dans cet article (çà remonte…).
Il s’agit de la classe OngoingStubbing
retournée par la méthode when
. Voici son contenu
modifié :
Cette deuxième partie touche à sa fin. Reste à assembler toutes les briques ensemble.
C’est le rôle de la classe déjà introduite MockHandler
, appelée à chaque appel de méthode sur notre mock :
- 8
-
Chaque
MockHandler
est associé à une instance de mock. C’est donc l’endroit idéal pour créer notreInvocationContainer
. - 13
- On dépile les matchers pour créer notre
InvocationMatcher
. - 16
- On enregistre un début possible de stubbing (sera confirmé plus tard).
- 26
- Si une réponse a déjà été enregistrée, on la retourne.
N’oublions pas également de revoir notre implémentation initiale du when
:
verify
Comparée à la deuxième, cette troisième partie s’annonce bien moins périlleuse.
Un premier aperçu...
Lors de l’exécution de cette ligne :
-
la méthode
times
est appelée. Cette factory se contente de créer une instance deVerificationMode
. -
la méthode
verify
est appelée. On mémorise le résultat attendu passé en paramètre (times(1)
) toujours dans notre objet globaleMockingProgress
. -
la méthode
anyString()
est appelée. Comme toujours, on mémorise les matchers pour plus tard. -
la méthode
registry#lookup(String)
est à nouveau appelée. On passe donc dans l’incontournableMockHandler
et c’est véritablement là que la vérification se fait. On dépile les matchers et on recherche les invocations réelles satisfaisant cette invocation "stubbée".
Commençons par introduire les VerificationMode
:
Comme pour les matchers, de nombreuses implémentations sont disponibles. Seule times
nous intéresse ici :
La vérification est aisée grâce à l’objet VerificationData
qui regroupe toutes les invocations
réelles et l’invocation "stubbée" (celle du verify
). Il suffit de rechercher toutes celles
correspondantes, et de comparer avec le nombre attendu.
Il nous faut également revoir notre implémentation initiale de la méthode verify
:
Ainsi que du MockingProgress
:
Sans oublier la classe centrale MockHandler
qui se complexifie à nouveau :
Terminé !
Bravo, vous venez de réaliser une version de Mockito opérationnelle en moins de 500 lignes. Le source complet est disponible ici.
Et c’est pas fini !
Bonus : le multithreading
La plupart des classes sont associées à une instance de mock. Chaque mock dispose de son propre
MockHandler
. La seule classe à synchroniser est la classe MockingProgress
, servant de
variable statique pour supporter l’API mockito. Avec l’aide de la classe Java ThreadLocal
, sa
synchronisation est presque transparente :
Chaque méthode commence systématiquement par récupérer l’instance associée au thread courant à l’aide de la
méthode get
de ThreadLocal
. Il ne nous reste plus qu’à remplacer dans les classes qui en
dépendent :
par :
Et le tour est joué !
Bonus : La gestion des erreurs
La gestion des erreurs passe bien évidemment par des exceptions mais ces exceptions ne sont pas lancées
directement à chaque erreur détectée. Au contraire, Mockito délègue cette responsabilité à une classe Reporter
qui regroupe l’ensemble des erreurs possibles. Pour chaque erreur, une méthode spécifique est proposée. Voici un
extrait de cette classe :
L’avantage de cette approche est de rendre facile la personnalisation des messages en un endroit unique, garantissant une cohérence globale sur les messages affichés à l'utilisateur. Pour l’utiliser :
A retenir
- Une API simple d’utilisation n’est pas synonyme d’une implémentation facile.
- Il est possible d’instancier une classe en Java sans passer par un constructeur à l’aide de librairies comme Objenesis
- Pour créer un proxy d’une classe sans interface, il faut recourir à de la manipulation de bytecode à l’aide de librairies comme Cglib ou Javassist.
- Cglib reste incontournable dans les frameworks existants mais sa pérennité n’est plus assurée. Nombreux sont les projets migrant vers Javassist.
- L’utilisation du
ThreadLocal
permet de partager un contexte global pour chaque thread de l’application.
A vous de forker
Notre découverte de Mockito nous aura entraîné dans les recoins les plus reculés de la librairie. Il reste pourtant tant de choses à découvrir. Voici quelques idées de fonctionnalités non présentées :
-
Mockito propose la vérification "inOrder" qui garantit que deux mocks sont sollicités dans un ordre bien
précis. Sachant que InvocationContainer est associé à un seul mock, comment cette fonctionnalité est-elle
implémentée ? Indice:
La classe
InvocationImpl
contient un attributsequenceNumber
. -
Mockito propose une méthoe
verifyZeroInteractions
, qui comme son nom l’indique, garantit qu’aucune interaction autre que celle préprogrammée n’a eu lieu sur un mock. Comment cela fonctionne-t-il ? Indice : La classeInvocationImpl
contient un attributverified
. -
Mockito propose pour un même appel
when
d’enchainer plusieurs appelsthenReturn
,thenThrow
, ... qui vont correspondre au résultat de la première, puis de la seconde, etc exécution de la méthode. Indice : comparerOngoingStubbingImpl
etConsecutiveStubbing
.