web-dev-qa-db-fra.com

Comment fonctionnent les matchers Mockito?

Les adaptateurs d'arguments Mockito (tels que any, argThat, eq, same et ArgumentCaptor.capture() ont un comportement très différent de ceux d'Hamcrest.

  • Les correspondants Mockito provoquent souvent une exception InvalidUseOfMatchersException, même dans le code qui s'exécute longtemps après l'utilisation de tout corrélateur.

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

  • Les correspondants Mockito peuvent provoquer une exception NullPointerException lors du remplacement de Answers ou de l'utilisation de (Integer) any() etc.

  • Le refactoring de code avec les adaptateurs Mockito peut, de certaines manières, produire des exceptions et un comportement inattendu, et peut échouer entièrement.

Pourquoi les adaptateurs Mockito sont-ils conçus ainsi et comment sont-ils mis en œuvre?

109
Jeff Bowman

Les adaptateurs Mockito sont des méthodes statiques et des appels à ces méthodes, qui représentent les arguments pendant les appels à when et verify.

Matchers Hamcrest (version archivée) (ou des matchers de style Hamcrest) sont des instances d'objet général et sans état, qui implémentent Matcher<T> et exposent une méthode matches(T) qui renvoie true si l'objet correspond aux critères de Matcher. . Ils sont censés être exempts d’effets secondaires et sont généralement utilisés dans des assertions telles que celle ci-dessous.

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

Il existe des correspondeurs Mockito distincts de ceux de style Hamcrest, pour que les descriptions des expressions correspondantes s'intègrent directement dans les invocations de méthodes : les correspondeurs Mockito retournent T où les méthodes Hamcrest Matcher renvoient des objets Matcher (de type Matcher<T>).

Les correspondants Mockito sont appelés via des méthodes statiques telles que eq, any, gt et startsWith sur org.mockito.Matchers et org.mockito.AdditionalMatchers. Il existe également des adaptateurs, qui ont changé d'une version à l'autre de Mockito:

  • Pour Mockito 1.x, Matchers décrit certains appels (tels que intThat ou argThat) sont des adaptateurs Mockito qui acceptent directement les adaptateurs de Hamcrest en tant que paramètres. ArgumentMatcher<T> étendu org.hamcrest.Matcher<T>, qui était utilisé dans la représentation interne de Hamcrest et était Classe de base du matcher Hamcrest à la place de toute sorte de matcher Mockito.
  • Pour Mockito 2.0+, Mockito n'a plus de dépendance directe à Hamcrest. Les appels Matchers sont définis sous la forme intThat ou argThat wrap ArgumentMatcher<T> objets qui n'implémentent plus org.hamcrest.Matcher<T> mais sont utilisés de manière similaire. Les adaptateurs Hamcrest tels que argThat et intThat sont toujours disponibles, mais sont passés à MockitoHamcrest .

Peu importe que les participants 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.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 matcher Mockito intThat enveloppe ce matcher de style Hamcrest et renvoie un int afin qu'il peut apparaître sous forme d'argument; Les correspondants Mockito tels que gt(9000) encapsuleraient cette expression dans un seul appel, comme dans la première ligne de l'exemple de code.

Que font/retournent les matchers?

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

Lorsque vous n'utilisez pas de correspondance d'arguments, Mockito enregistre vos valeurs d'argument et les compare à 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 matcher comme any ou gt (supérieur à), Mockito stocke un objet matcher qui force Mockito à ignorer cette vérification d'égalité et à appliquer la correspondance de votre choix. Dans le cas de argumentCaptor.capture(), il stocke un adaptateur qui enregistre son argument à la place pour une inspection ultérieure.

Les correspondeurs renvoient valeurs factices tels que zéro, des collections vides ou null. Mockito tente de renvoyer une valeur factice sûre et appropriée, telle que 0 pour anyInt() ou any(Integer.class) ou un List<String> vide pour anyListOf(String.class). Cependant, en raison de l'effacement du type, Mockito ne dispose d'aucune information de type pour renvoyer une valeur autre que null pour any() ou argThat(...), ce qui peut provoquer une exception NullPointerException si vous essayez de "décompresser automatiquement" une valeur primitive null.

Des correspondants tels que eq et gt prennent des valeurs de paramètre; dans l'idéal, ces valeurs devraient être calculées avant le début du remplacement/de la vérification. Appeler un simulacre au milieu d'un autre appel peut gêner le stubbing.

Les méthodes Matcher ne peuvent pas être utilisées comme valeurs de retour; Par exemple, il n'y a pas moyen de prononcer thenReturn(anyInt()) ou thenReturn(any(Foo.class)) dans Mockito. Mockito doit savoir exactement quelle instance renvoyer dans les appels de stubbing et ne choisira pas de valeur de retour arbitraire pour vous.

Détails d'implémentation

Les correspondants sont stockés (en tant que modèles d'objet de style Hamcrest) dans une pile contenue dans une classe appelée ArgumentMatcherStorage . MockitoCore et les correspondants possèdent chacun une instance ThreadSafeMockingProgress , laquelle de manière statique contient un ThreadLocal contenant des occurrences MockingProgress. C'est ce MockingProgressImpl qui contient un béton ArgumentMatcherStorageImpl . Par conséquent, l'état de simulation et de correspondance est statique, mais son étendue de thread est cohérente entre les classes Mockito et Matchers.

La plupart des appels d'appariement s'ajoutent uniquement à cette pile, à l'exception des correspondants tels que and, or ET not . Ceci correspond parfaitement à (et s'appuie sur) le 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. Supprimez gt(10) et lt(20) et ajoutez and(gt(10), lt(20)).
  5. Appelez foo.quux(0, 0), qui (sauf indication contraire) renvoie la valeur par défaut false. En interne, Mockito marque quux(int, int) comme l'appel le plus récent.
  6. Appelez when(false), qui rejette son argument et prépare la méthode stub quux(int, int) identifiée dans 5. Les deux seuls états valides sont avec une longueur de pile égale à 0 (égalité) ou 2 (correspondants), et il existe deux correspondants la pile (étapes 1 et 4), de sorte que Mockito stubs la méthode avec un matcher any() pour son premier argument et and(gt(10), lt(20)) pour son deuxième argument, puis 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 int matcher sur la pile. Par conséquent, si vous utilisez un seul matcher, vous devez faire correspondre tous les arguments.

  • L'ordre d'appel n'est pas seulement important, c'est ce qui fait que tout fonctionne . Extraire les correspondants des variables ne fonctionne généralement pas, car cela modifie généralement l'ordre des appels. Extraire les correspondants des 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 pour que Mockito ne puisse pas la contrôler très attentivement. Il ne peut vérifier la pile que lorsque vous interagissez avec Mockito ou une maquette, et doit accepter des joueurs sans savoir s'ils ont été utilisés immédiatement ou ont été abandonnés accidentellement. En théorie, la pile devrait 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 en fait la méthode en question, qui lève une exception si vous avez stubé la méthode pour qu'elle lève une exception (ou si vous exigez des valeurs non nulles ou non nulles). doReturn et doAnswer (etc) do not invoque la méthode réelle et constitue souvent une alternative utile.

  • Si vous aviez appelé une méthode fictive au milieu de la stubbing (par exemple pour calculer une réponse pour un matcher eq), Mockito vérifierait la longueur de la pile avec que appelera à la place et échouera 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 sur la pile . L'appel à la méthode final ne peut pas lever une exception, mais vous pouvez obtenir une exception InvalidUseOfMatchersException des correspondants égarés lors de votre prochaine interaction avec une maquette.

Problèmes communs

  • InvalidUseOfMatchersException:

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

    • Vérifiez que vous n'appelez pas une maquette 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, il se peut que ce soit la seule fois où vous réalisez que la méthode dont vous vous moquez est définitive.

  • NullPointerException avec des arguments primitifs:(Integer) any() renvoie null tant 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 renverra zéro et ignorera également l'étape de boxe automatique.

  • NullPointerException ou d’autres exceptions: Les appels à when(foo.bar(any())).thenReturn(baz) seront réellement call foo.bar(null), ce à quoi vous auriez peut-être dû renvoyer une exception lorsque recevoir un argument nul. Passer à doReturn(baz).when(foo).bar(any())ignore le comportement de stubbed .

Dépannage général

  • Utilisez MockitoJUnitRunner , ou appelez explicitement validateMockitoUsage dans votre méthode tearDown ou @After (ce que le coureur ferait automatiquement pour vous). Cela aidera à déterminer si vous avez mal utilisé des matchers.

  • Pour les besoins du débogage, ajoutez directement les appels à validateMockitoUsage dans votre code. Cela jettera si vous avez quelque chose sur la pile, ce qui est un bon avertissement d'un mauvais symptôme.

217
Jeff Bowman

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

Si un appel à une méthode correspond à plusieurs appels formés when simulés, l'ordre des appels when est important et doit être du plus large au plus spécifique. À partir d'un exemple de Jeff:

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);

est l'ordre qui assure 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, le résultat sera toujours true.

8
tibtof