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

89
demandé sur Jeff Bowman 2014-04-03 00:37:16

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 que intThat ou argThat) sont des matchers Mockito qui acceptent directement les matchers Hamcrest comme paramètres. ArgumentMatcher<T> extended org.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 comme intThat ou argThat envelopper ArgumentMatcher<T> objets qui n'implémentent plus org.hamcrest.Matcher<T> mais qui sont utilisés de manière similaire. Les adaptateurs Hamcrest tels que argThat et intThat sont toujours disponibles, mais ont été déplacés vers MockitoHamcrest 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.

Les

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

Les

Matchers 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:

  1. Ajouter anyInt() à la pile.
  2. Ajouter gt(10) à la pile.
  3. Ajouter lt(20) à la pile.
  4. Supprime gt(10) et lt(20) et ajoutez and(gt(10), lt(20)).
  5. appelez foo.quux(0, 0), qui (sauf si elle est tronquée) renvoie la valeur par défaut false. En interne, Mockito marque quux(int, int) comme l'appel le plus récent.
  6. appel when(false), qui rejette son argument et se prépare à stub méthode quux(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 un any() matcher pour son premier argument et and(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) et quux(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 ou verify, mais Mockito ne peut pas vérifier cela automatiquement. Vous pouvez vérifier manuellement avec Mockito.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 et doAnswer (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 ou verify 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 que any(Integer.class) renvoie 0; cela peut provoquer un NullPointerException si vous attendez un int au lieu d'un entier. Dans tous les cas, préférez anyInt(), 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 appeler foo.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 explicitementvalidateMockitoUsage dans votre méthode tearDown 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.

192
répondu Jeff Bowman 2017-07-09 12:00:17

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.

6
répondu tibtof 2015-11-05 14:58:56