Comment fonctionnent les matchers Mockito?
Mockito argument de rapprochement (comme any
, argThat
, eq
, same
, et ArgumentCaptor.capture()
) se comportent très différemment de Hamcrest de rapprochement.
Les correspondances Mockito provoquent fréquemment Invalideeofmatchersexception, même dans le code qui s'exécute longtemps après l'utilisation des correspondances.
Les matchers Mockito sont redevables de règles étranges, telles que l'utilisation de matchers Mockito pour tous les arguments si un argument dans une méthode donnée utilise un matcher.
Mockito les matchers peuvent provoquer une exception NullPointerException lors de la substitution de
Answer
S ou lors de l'utilisation de(Integer) any()
etc.Refactoring code avec Mockito matchers de certaines façons peut produire des exceptions et un comportement inattendu, et peut échouer entièrement.
Pourquoi les matchers Mockito sont-ils conçus comme ça, et comment sont-ils implémentés?
2 réponses
Les matchers Mockito sont des méthodes statiques et des appels à ces méthodes, qui servent d'arguments lors des appels à when
et verify
.
Les matchers Hamcrest (version archivée) (ou les matchers de style Hamcrest) sont des instances d'objets sans état et à usage général qui implémentent Matcher<T>
et exposent une méthode matches(T)
qui renvoie true si l'objet correspond aux critères du Matcher. Ils sont destinés à être exempt d'effets secondaires, et sont généralement utilisés dans des affirmations comme celui ci-dessous.
/* Mockito */ verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));
Les correspondances Mockito existent, séparées des correspondances de style Hamcrest, de sorte que les descriptions des expressions correspondantes s'intègrent directement dans les invocations de méthode: Mockito matchers return T
où les méthodes Hamcrest matcher renvoient des objets Matcher (de type Matcher<T>
).
Mockito les allumettes sont invoqués par des méthodes statiques telles que eq
, any
, gt
, et startsWith
sur org.mockito.Matchers
et org.mockito.AdditionalMatchers
. Il y a aussi des adaptateurs, qui ont changé à travers les versions de Mockito:
- Pour Mockito 1.x,
Matchers
certains appels (tels queintThat
ouargThat
) sont des matchers Mockito qui acceptent directement les matchers Hamcrest comme paramètres.ArgumentMatcher<T>
extendedorg.hamcrest.Matcher<T>
, qui était utilisé dans la représentation Hamcrest interne et était une classe de base Hamcrest matcher au lieu de toute sorte de matcher Mockito. - pour Mockito 2.0+, Mockito ne dépend plus directement de Hamcrest.
Matchers
appelle commeintThat
ouargThat
envelopperArgumentMatcher<T>
objets qui n'implémentent plusorg.hamcrest.Matcher<T>
mais qui sont utilisés de manière similaire. Les adaptateurs Hamcrest tels queargThat
etintThat
sont toujours disponibles, mais ont été déplacés versMockitoHamcrest
au lieu de cela.
Peu importe si les matchers sont Hamcrest ou simplement Hamcrest-style, ils peuvent être adaptés comme ceci:
/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));
Dans la déclaration ci-dessus: foo.setPowerLevel
est une méthode qui accepte un int
. is(greaterThan(9000))
renvoie un Matcher<Integer>
, qui ne fonctionnerait pas comme un argument setPowerLevel
. Le Mockito matcher intThat
enveloppe ce Matcher de style Hamcrest et renvoie un int
de sorte qu'il peut apparaître comme un argument; les matchers Mockito comme gt(9000)
envelopperaient cette expression entière dans un seul appel, comme dans la première ligne de code d'exemple.
Ce que les matchers font / retournent
when(foo.quux(3, 5)).thenReturn(true);
Lorsque vous n'utilisez pas de matchers d'arguments, Mockito enregistre vos valeurs d'arguments et les compare avec leurs méthodes equals
.
when(foo.quux(eq(3), eq(5))).thenReturn(true); // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different
Lorsque vous appelez un comparateur comme any
ou gt
(plus que), Mockito stocke un objet matcher qui amène Mockito à ignorer cette vérification d'égalité et à appliquer votre correspondance de choix. Dans le cas de argumentCaptor.capture()
, Il stocke un matcher qui enregistre son argument à la place pour une inspection ultérieure.
Les Matchers renvoient des valeurs fictives telles que zéro, des collections vides ou null
. Mockito essaie de renvoyer une valeur fictive sûre et appropriée, comme 0 pour anyInt()
ou any(Integer.class)
ou un List<String>
vide pour anyListOf(String.class)
. En raison de l'effacement de type, cependant, Mockito manque d'informations de type pour renvoie toute valeur sauf null
pour any()
ou argThat(...)
, ce qui peut provoquer une exception NullPointerException si vous essayez de "déballer automatiquement" une valeur primitive null
.
Matchers comme eq
et gt
prennent des valeurs de paramètre; idéalement, ces valeurs devraient être calculées avant le début du stubbing/vérification. Appeler un simulacre au milieu de se moquer d'un autre appel peut interférer avec le stubbing.
Les méthodes Matcher ne peuvent pas être utilisées comme valeurs de retour; il n'y a aucun moyen d'exprimer thenReturn(anyInt())
ou thenReturn(any(Foo.class))
dans Mockito, pour instance. Mockito doit savoir exactement quelle instance renvoyer dans les appels stubbing, et ne choisira pas une valeur de retour arbitraire pour vous.
Détails de mise en œuvre
LesMatchers sont stockés (en tant que matchers d'objets de style Hamcrest) dans une pile contenue dans une classe appelée ArgumentMatcherStorage. MockitoCore et Matchers possèdent chacun une instance ThreadSafeMockingProgress , quistatiquement contient une instance ThreadLocal contenant des instances MockingProgress. C'est ce MockingProgressImpl qui contient un ArgumentMatcherStorageImpl concret. Par conséquent, l'état mock et matcher est statique mais la portée des threads est constante entre les classes Mockito et Matchers.
La plupart des appels de matcher ne s'ajoutent qu'à cette pile, avec une exception pour les matchers comme and
, or
, et not
. Cela correspond parfaitement (et repose sur) l'ordre d'évaluation de Java , qui évalue les arguments de gauche à droite avant invoquer une méthode:
when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6] [5] [1] [4] [2] [3]
Ce sera:
- Ajouter
anyInt()
à la pile. - Ajouter
gt(10)
à la pile. - Ajouter
lt(20)
à la pile. - Supprime
gt(10)
etlt(20)
et ajoutezand(gt(10), lt(20))
. - appelez
foo.quux(0, 0)
, qui (sauf si elle est tronquée) renvoie la valeur par défautfalse
. En interne, Mockito marquequux(int, int)
comme l'appel le plus récent. - appel
when(false)
, qui rejette son argument et se prépare à stub méthodequux(int, int)
identifié dans 5. La seule deux états valides sont avec la longueur de la pile 0 (égalité) ou 2 (matchers), et il y a deux matchers sur la pile (étapes 1 et 4), donc Mockito stubs la méthode avec unany()
matcher pour son premier argument etand(gt(10), lt(20))
pour son deuxième argument et efface la pile.
Cela démontre quelques règles:
Mockito ne peut pas faire la différence entre
quux(anyInt(), 0)
etquux(0, anyInt())
. Ils ressemblent tous deux à un appel àquux(0, 0)
avec un int matcher sur la pile. Par conséquent, si vous utilisez un matcher, vous devez faire correspondre tous les arguments.-
L'Ordre des appels n'est pas seulement important, c'est ce qui fait que tout cela fonctionne. L'extraction de matchers vers des variables ne fonctionne généralement pas, car elle modifie généralement l'ordre des appels. Extraire les matchers aux méthodes, cependant, fonctionne très bien.
int between10And20 = and(gt(10), lt(20)); /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true); // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt(). public static int anyIntBetween10And20() { return and(gt(10), lt(20)); } /* OK */ when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true); // The helper method calls the matcher methods in the right order.
La pile change assez souvent que Mockito ne peut pas la surveiller très soigneusement. Il ne peut vérifier la pile que lorsque vous interagissez avec Mockito ou une maquette, et doit acceptez les allumettes sans savoir si elles sont utilisées immédiatement ou abandonnées accidentellement. En théorie, la pile doit toujours être vide en dehors d'un appel à
when
ouverify
, mais Mockito ne peut pas vérifier cela automatiquement. Vous pouvez vérifier manuellement avecMockito.validateMockitoUsage()
.Dans un appel à
when
, Mockito appelle réellement la méthode en question, qui lancera une exception si vous avez tronqué la méthode pour lancer une exception (ou exiger des valeurs non nulles ou non nulles).doReturn
etdoAnswer
(etc) ne pas invoquer la méthode et sont souvent une alternative utile.Si vous aviez appelé une méthode simulée au milieu du stubbing (par exemple pour calculer une réponse pour un
eq
matcher), Mockito vérifierait la longueur de la pile par rapport à cet appel à la place, et échouerait probablement.Si vous essayez de faire quelque chose de mal, comme stubbing / vérification d'une méthode finale , Mockito appellera la méthode réelle et laissera également des correspondances supplémentaires la pile. L'appel de méthode
final
ne peut pas lancer d'exception, mais vous pouvez obtenir une Invalideeofmatchersexception {[99] } des correspondeurs errants lorsque vous interagissez ensuite avec un simulacre.
Problèmes Communs
-
Invalideduseofmatchersexception:
Vérifiez que chaque argument a exactement un appel de matcher, si vous utilisez des matchers, et que vous n'avez pas utilisé de matcher en dehors d'un
when
ouverify
appel. Les Matchers ne doivent jamais être utilisés comme valeurs de retour tronquées ou comme champs/variables.Vérifiez que vous n'appelez pas un simulacre dans le cadre de la fourniture d'un argument matcher.
Vérifiez que vous n'essayez pas de stub / vérifier une méthode finale avec un matcher. C'est un excellent moyen de laisser un matcher sur la pile, et à moins que votre méthode finale ne lance une exception, c'est peut-être la seule fois où vous réalisez que la méthode que vous vous moquez est final.
NullPointerException avec des arguments primitifs:
(Integer) any()
renvoie null tandis queany(Integer.class)
renvoie 0; cela peut provoquer unNullPointerException
si vous attendez unint
au lieu d'un entier. Dans tous les cas, préférezanyInt()
, qui retournera zéro et sautera également l'étape d'auto-boxe.NullPointerException ou d'autres exceptions: Appels à
when(foo.bar(any())).thenReturn(baz)
sera effectivement appelerfoo.bar(null)
, dont vous pourriez avoir frappé à jeter une exception lors de la réception d'un argument null. Passer àdoReturn(baz).when(foo).bar(any())
ignore le comportement tronqué .
Dépannage Général
Utilisez MockitoJUnitRunner , ou appelez explicitement
validateMockitoUsage
dans votre méthodetearDown
ou@After
(ce que le coureur ferait automatiquement pour vous). Cela aidera à déterminer si vous avez mal utilisé matchers.Pour le débogage, Ajoutez directement des appels à
validateMockitoUsage
dans votre code. Cela va jeter si vous avez quelque chose sur la pile, ce qui est un bon avertissement d'un mauvais symptôme.
Juste un petit ajout à L'excellente réponse de Jeff Bowman, car j'ai trouvé cette question lors de la recherche d'une solution à l'un de mes propres problèmes:
Si un appel à une méthode correspond à plusieurs appels when
formés d'un mock, l'ordre des appels when
est important et devrait aller du plus large au plus spécifique. À partir de L'un des exemples de Jeff:
when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);
Est l'ordre qui garantit le résultat (probablement) souhaité:
foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false
Si vous inversez les appels when alors le résultat serait toujours true
.