Comment fonctionnent les matchers Mockito?

122

Matchers argument Mockito (tels que any, argThat, eq, sameet ArgumentCaptor.capture()) se comportent très différemment des matchers Hamcrest.

  • Les correspondances Mockito provoquent fréquemment une exception InvalidUseOfMatchersException, même dans le code qui s'exécute longtemps après l'utilisation des correspondances.

  • Les matchers Mockito sont redevables à des règles étranges, comme ne nécessitant l'utilisation de matchers Mockito pour tous les arguments que si un argument dans une méthode donnée utilise un matcher.

  • Les correspondances Mockito peuvent provoquer une exception NullPointerException lors de la substitution de Answers ou lors de l'utilisation, (Integer) any()etc.

  • La refactorisation du code avec des correspondants Mockito de certaines manières peut produire des exceptions et un comportement inattendu, et peut échouer complètement.

Pourquoi les matchers Mockito sont-ils conçus comme ça et comment sont-ils mis en œuvre?

Jeff Bowman
la source

Réponses:

236

Les matchers Mockito sont des méthodes statiques et des appels à ces méthodes, qui remplacent les arguments lors des appels à whenet verify.

Les matchers Hamcrest (version archivée) (ou matchers de style Hamcrest) sont des instances d'objet sans état à 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 exempts d'effets secondaires et sont généralement utilisés dans des affirmations telles que celle ci-dessous.

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Les correspondants Mockito existent, séparés des correspondants de style Hamcrest, de sorte que les descriptions des expressions correspondantes s'intègrent directement dans les appels de méthode : les correspondants Mockito retournent là où les méthodes de mise en correspondance Hamcrest renvoient des objets Matcher T(de type Matcher<T>).

Matchers Mockito sont invoquées par des méthodes statiques telles que eq, any, gtet startsWithsur org.mockito.Matcherset org.mockito.AdditionalMatchers. Il existe également des adaptateurs, qui ont changé d'une version à l'autre de Mockito:

  • Pour Mockito 1.x, Matcherscertains appels (tels que intThatou argThat) sont des correspondants Mockito qui acceptent directement les correspondants Hamcrest comme paramètres. ArgumentMatcher<T>élargie org.hamcrest.Matcher<T>, qui a été utilisé dans la représentation interne et Hamcrest était une classe de base de matcher Hamcrest à la place de toute sorte de Mockito matcher.
  • Pour Mockito 2.0+, Mockito n'a plus de dépendance directe sur Hamcrest. Matchersles appels formulés en tant qu'objets enveloppants intThatou qui ne sont plus implémentés mais qui sont utilisés de manière similaire. Les adaptateurs Hamcrest tels que et sont toujours disponibles, mais ont été déplacés vers .argThatArgumentMatcher<T>org.hamcrest.Matcher<T>argThatintThatMockitoHamcrest

Que les matchers soient de type Hamcrest ou simplement de style Hamcrest, ils peuvent être adaptés comme suit:

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

Dans la déclaration ci-dessus: foo.setPowerLevelest une méthode qui accepte un int. is(greaterThan(9000))renvoie a Matcher<Integer>, qui ne fonctionnerait pas comme setPowerLevelargument. Le matcher Mockito intThatencapsule ce Matcher de style Hamcrest et renvoie un intpour qu'il puisse apparaître comme un argument; Les correspondances Mockito comme gt(9000)envelopperaient toute cette expression en un seul appel, comme dans la première ligne de l'exemple de code.

Ce que les matchers font / retournent

when(foo.quux(3, 5)).thenReturn(true);

Lorsque vous n'utilisez pas de correspondance d'argument, Mockito enregistre vos valeurs d'argument et les compare avec leurs equalsméthodes.

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 une correspondance comme anyou gt(supérieure à), Mockito stocke un objet de correspondance qui oblige Mockito à ignorer cette vérification d'égalité et à appliquer la correspondance de votre choix. Dans ce cas, argumentCaptor.capture()il stocke un matcher qui enregistre son argument à la place pour une inspection ultérieure.

Les correspondants renvoient des valeurs factices 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 une valeur vide List<String>pour anyListOf(String.class). En raison de l'effacement de type, cependant, Mockito n'a pas d'informations de type pour renvoyer n'importe quelle valeur sauf nullpour any()ou argThat(...), ce qui peut provoquer une exception NullPointerException si vous essayez de "décompresser automatiquement" une nullvaleur primitive.

Les correspondants aiment eqet gtprennent des valeurs de paramètres; idéalement, ces valeurs devraient être calculées avant le début du stubbing / vérification. Appeler un simulacre en se moquant 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))de Mockito, par exemple. Mockito a besoin de savoir exactement quelle instance retourner dans les appels de stubbing et ne choisira pas une valeur de retour arbitraire pour vous.

Détails d'implémentation

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 , qui contient statiquement un ThreadLocal contenant des instances MockingProgress. C'est ce MockingProgressImpl qui contient un ArgumentMatcherStorageImpl concret . Par conséquent, l'état de simulation et de correspondance est statique mais à la portée des threads de manière cohérente entre les classes Mockito et Matchers.

La plupart avec une exception pour les matchers comme les appels matcher ne font qu'ajouter à cette pile, and, oretnot . Cela correspond parfaitement à (et repose sur) l' ordre d'évaluation de Java , qui évalue les arguments de gauche à droite avant d'appeler une méthode:

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

Cette volonté:

  1. Ajoutez anyInt()à la pile.
  2. Ajoutez gt(10)à la pile.
  3. Ajoutez lt(20)à la pile.
  4. Retirez gt(10)et lt(20)et ajouter and(gt(10), lt(20)).
  5. Call foo.quux(0, 0), qui (sauf stubbed autrement) renvoie la valeur par défaut false. En interne, Mockito marque quux(int, int)l'appel le plus récent.
  6. Call when(false), qui rejette son argument et prépare la méthode stub quux(int, int)identifiée en 5. Les deux seuls états valides sont avec la longueur de 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 les deux à un appel à quux(0, 0)avec un matcher int 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 fonctionne . L'extraction de correspondances vers des variables ne fonctionne généralement pas, car elle modifie généralement l'ordre des appels. Cependant, l'extraction des correspondances aux méthodes 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 pour que Mockito ne puisse pas la contrôler très soigneusement. Il ne peut vérifier la pile que lorsque vous interagissez avec Mockito ou un simulacre, et doit accepter des correspondants sans savoir s'ils sont utilisés immédiatement ou abandonnés accidentellement. En théorie, la pile devrait toujours être vide en dehors d'un appel à whenou verify, mais Mockito ne peut pas le vérifier automatiquement. Vous pouvez vérifier manuellement avec Mockito.validateMockitoUsage().

  • Dans un appel à when, Mockito appelle en fait la méthode en question, qui lèvera une exception si vous avez stubblé la méthode pour lever une exception (ou si vous avez besoin de valeurs non nulles ou non nulles). doReturnet doAnswer(etc.) n'invoquent pas la méthode réelle et sont souvent une alternative utile.

  • Si vous aviez appelé une méthode fictive au milieu du stubbing (par exemple pour calculer une réponse pour un eqmatcher), Mockito vérifierait la longueur de pile par rapport à cet appel à la place, et échouerait probablement.

  • Si vous essayez de faire quelque chose de mal, comme le stubbing / vérifier une méthode finale , Mockito appellera la méthode réelle et laissera également des correspondants supplémentaires sur la pile . L' finalappel de méthode ne peut pas lever d' exception, mais vous pouvez obtenir une InvalidUseOfMatchersException à partir des correspondances parasites lors de votre prochaine interaction avec une simulation.

Problèmes communs

  • InvalidUseOfMatchersException :

    • Vérifiez que chaque argument a exactement un appel de correspondance, si vous utilisez des correspondances, et que vous n'avez pas utilisé de correspondance en dehors d'un appel whenou verify. Les correspondants ne doivent jamais être utilisés comme valeurs de retour ou champs / variables stubbed.

    • Vérifiez que vous n'appelez pas une simulation dans le cadre de la fourniture d'un argument de correspondance.

    • 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 lève une exception, c'est peut-être la seule fois où vous réalisez que la méthode que vous vous moquez est définitive.

  • NullPointerException avec arguments primitifs: (Integer) any() renvoie null tandis que any(Integer.class)retourne 0; cela peut provoquer un NullPointerExceptionsi vous attendez un intau lieu d'un entier. Dans tous les cas, préférez anyInt(), qui renverra zéro et sautera également l'étape d'auto-boxing.

  • NullPointerException ou d' autres exceptions: appels à when(foo.bar(any())).thenReturn(baz)se fait appeler foo.bar(null) , que vous pourriez avoir bouchonné à jeter une exception lors de la réception d' un argument nul. Le passage à doReturn(baz).when(foo).bar(any()) ignore le comportement stubbed .

Dépannage général

  • Utilisez MockitoJUnitRunner , ou appelez explicitement validateMockitoUsagevotre méthode tearDownou @After(ce que le coureur ferait pour vous automatiquement). Cela aidera à déterminer si vous avez mal utilisé les correspondants.

  • À des fins de débogage, ajoutez validateMockitoUsagedirectement des appels à dans votre code. Cela lancera si vous avez quelque chose sur la pile, ce qui est un bon avertissement d'un mauvais symptôme.

Jeff Bowman
la source
2
Merci pour cet article. Une exception NullPointerException avec le format when / thenReturn me posait des problèmes, jusqu'à ce que je la change en doReturn / when.
yngwietiger
11

Juste un petit ajout à l'excellente réponse de Jeff Bowman, car j'ai trouvé cette question en cherchant une solution à l'un de mes propres problèmes:

Si un appel à une méthode correspond à plusieurs whenappels entraînés simulés , l'ordre des whenappels est important et doit être 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 quand, le résultat sera toujours true.

tibtof
la source
2
Bien que ce soit des informations utiles, elles concernent le stubbing, pas les matchers , donc cela peut ne pas avoir de sens sur cette question. L'ordre est important, mais seulement dans la mesure où la dernière chaîne correspondante définie l'emporte : cela signifie que les stubs coexistants sont souvent déclarés du plus spécifique au moins, mais dans certains cas, vous pouvez souhaiter un remplacement très large du comportement spécifiquement simulé dans un seul scénario de test , auquel cas une définition large peut devoir venir en dernier.
Jeff Bowman
1
@JeffBowman J'ai pensé que cela avait du sens sur cette question car la question concerne les matchers mockito et les matchers peuvent être utilisés lors du stubbing (comme dans la plupart de vos exemples). Depuis la recherche d'une explication sur google m'a amené à cette question, je pense qu'il est utile d'avoir cette information ici.
tibt du