Un guide des attentes JMockit

1. Introduction

Cet article est le deuxième volet de la série JMockit. Vous voudrez peut-être lire le premier article car nous supposons que vous êtes déjà familiarisé avec les bases de JMockit.

Aujourd'hui, nous allons approfondir et nous concentrer sur les attentes. Nous montrerons comment définir des correspondances d'arguments plus spécifiques ou génériques et des moyens plus avancés de définir des valeurs.

2. Correspondance des valeurs d'argument

Les approches suivantes s'appliquent à la fois aux attentes et aux vérifications .

2.1. Champs "Tous"

JMockit propose un ensemble de champs utilitaires pour rendre la correspondance d'arguments plus générique. L'un de ces utilitaires sont les champs anyX .

Ceux-ci vérifieront que toute valeur a été passée et qu'il y en a une pour chaque type primitif (et la classe wrapper correspondante), une pour les chaînes et une "universelle" de type Object .

Voyons un exemple:

public interface ExpectationsCollaborator { String methodForAny1(String s, int i, Boolean b); void methodForAny2(Long l, List lst); } @Test public void test(@Mocked ExpectationsCollaborator mock) throws Exception { new Expectations() {{ mock.methodForAny1(anyString, anyInt, anyBoolean); result = "any"; }}; Assert.assertEquals("any", mock.methodForAny1("barfooxyz", 0, Boolean.FALSE)); mock.methodForAny2(2L, new ArrayList()); new FullVerifications() {{ mock.methodForAny2(anyLong, (List) any); }}; }

Vous devez tenir compte du fait que lorsque vous utilisez le champ any , vous devez le convertir en type attendu. La liste complète des champs est présente dans la documentation.

2.2. Méthodes «avec»

JMockit fournit également plusieurs méthodes pour aider à la correspondance d'arguments génériques. Ce sont les méthodes withX .

Ceux-ci permettent une correspondance un peu plus avancée que les champs anyX . Nous pouvons voir un exemple ici dans lequel nous définirons une attente pour une méthode qui sera déclenchée avec une chaîne contenant foo , un entier différent de 1, un booléen non nul et toute instance de la classe List :

public interface ExpectationsCollaborator { String methodForWith1(String s, int i); void methodForWith2(Boolean b, List l); } @Test public void testForWith(@Mocked ExpectationsCollaborator mock) throws Exception { new Expectations() {{ mock.methodForWith1(withSubstring("foo"), withNotEqual(1)); result = "with"; }}; assertEquals("with", mock.methodForWith1("barfooxyz", 2)); mock.methodForWith2(Boolean.TRUE, new ArrayList()); new Verifications() {{ mock.methodForWith2(withNotNull(), withInstanceOf(List.class)); }}; }

Vous pouvez voir la liste complète des withX méthodes sur la documentation de JMockit.

Tenez compte du fait que le spécial avec (Delegate) et withArgThat (Matcher) sera couvert dans leur propre sous-section.

2.3. Null n'est pas nul

Il est bon de comprendre le plus tôt possible que null n'est pas utilisé pour définir un argument pour lequel null a été passé à un simulacre.

En fait, null est utilisé comme sucre syntaxique pour définir que tout objet sera passé (il ne peut donc être utilisé que pour les paramètres de type référence). Pour vérifier spécifiquement qu'un paramètre donné reçoit la référence null , le matcher withNull () peut être utilisé.

Pour l'exemple suivant, nous définirons le comportement d'un simulacre, qui devrait être déclenché lorsque les arguments passés sont: n'importe quelle chaîne, n'importe quelle liste et une référence nulle :

public interface ExpectationsCollaborator { String methodForNulls1(String s, List l); void methodForNulls2(String s, List l); } @Test public void testWithNulls(@Mocked ExpectationsCollaborator mock){ new Expectations() {{ mock.methodForNulls1(anyString, null); result = "null"; }}; assertEquals("null", mock.methodForNulls1("blablabla", new ArrayList())); mock.methodForNulls2("blablabla", null); new Verifications() {{ mock.methodForNulls2(anyString, (List) withNull()); }}; }

Notez la différence: null signifie n'importe quelle liste et withNull () signifie une référence nulle à une liste. En particulier, cela évite d'avoir à convertir la valeur en type de paramètre déclaré (voir que le troisième argument devait être converti mais pas le second).

La seule condition pour pouvoir l'utiliser est qu'au moins un argument matcher explicite ait été utilisé pour l'attente (soit une méthode with , soit un champ any ).

2.4. Champ "Times"

Parfois, nous voulons limiter le nombre d'appels attendus pour une méthode simulée. Pour cela, JMockit a les mots réservés times , minTimes et maxTimes (les trois n'autorisent que les entiers non négatifs).

public interface ExpectationsCollaborator { void methodForTimes1(); void methodForTimes2(); void methodForTimes3(); } @Test public void testWithTimes(@Mocked ExpectationsCollaborator mock) { new Expectations() {{ mock.methodForTimes1(); times = 2; mock.methodForTimes2(); }}; mock.methodForTimes1(); mock.methodForTimes1(); mock.methodForTimes2(); mock.methodForTimes3(); mock.methodForTimes3(); mock.methodForTimes3(); new Verifications() {{ mock.methodForTimes3(); minTimes = 1; maxTimes = 3; }}; }

Dans cet exemple, nous avons défini qu'exactement deux invocations (pas une, pas trois, exactement deux) de methodForTimes1 () doivent être effectuées en utilisant la ligne times = 2; .

Ensuite, nous avons utilisé le comportement par défaut (si aucune contrainte de répétition n'est donnée minTimes = 1; est utilisée) pour définir qu'au moins un appel sera fait à methodForTimes2 ().

Enfin, en utilisant minTimes = 1; suivi de maxTimes = 3; nous avons défini qu'entre une et trois invocations se produiraient à methodForTimes3 () .

Tenez compte du fait que minTimes et maxTimes peuvent être spécifiés pour la même attente, à condition que minTimes soit affecté en premier. En revanche, les temps ne peuvent être utilisés que seuls.

2.5. Correspondance d'arguments personnalisés

Parfois, la correspondance d'arguments n'est pas aussi directe que la simple spécification d'une valeur ou l'utilisation de certains des utilitaires prédéfinis ( anyX ou withX ).

Pour que les cas, JMockit repose sur de Hamcrest matcher interface. Il vous suffit de définir un matcher pour le scénario de test spécifique et de l'utiliser avec un appel withArgThat () .

Voyons un exemple pour faire correspondre une classe spécifique à un objet passé:

public interface ExpectationsCollaborator { void methodForArgThat(Object o); } public class Model { public String getInfo(){ return "info"; } } @Test public void testCustomArgumentMatching(@Mocked ExpectationsCollaborator mock) { new Expectations() {{ mock.methodForArgThat(withArgThat(new BaseMatcher() { @Override public boolean matches(Object item) { return item instanceof Model && "info".equals(((Model) item).getInfo()); } @Override public void describeTo(Description description) { } })); }}; mock.methodForArgThat(new Model()); }

3. Valeurs renvoyées

Regardons maintenant les valeurs de retour; gardez à l'esprit que les approches suivantes s'appliquent uniquement aux attentes car aucune valeur de retour ne peut être définie pour les vérifications .

3.1. Résultat et retours (…)

Lorsque vous utilisez JMockit, vous avez trois façons différentes de définir le résultat attendu de l'appel d'une méthode simulée. Des trois, nous parlerons maintenant des deux premiers (les plus simples) qui couvriront sûrement 90% des cas d'usage quotidien.

These two are the result field and the returns(Object…) method:

  • With the result field, you can define one return value for any non-void returning mocked method. This return value can also be an exception to be thrown (this time working for both non-void and void returning methods).
    • Several result field assignations can be done in order to return more than one value for more than one method invocations (you can mix both return values and errors to be thrown).
    • The same behaviour will be achieved when assigning to result a list or an array of values (of the same type than the return type of the mocked method, NO exceptions here).
  • The returns(Object…) method is syntactic sugar for returning several values of the same time.

This is more easily shown with a code snippet:

public interface ExpectationsCollaborator{ String methodReturnsString(); int methodReturnsInt(); } @Test public void testResultAndReturns(@Mocked ExpectationsCollaborator mock) { new Expectations() {{ mock.methodReturnsString(); result = "foo"; result = new Exception(); result = "bar"; returns("foo", "bar"); mock.methodReturnsInt(); result = new int[]{1, 2, 3}; result = 1; }}; assertEquals("Should return foo", "foo", mock.methodReturnsString()); try { mock.methodReturnsString(); fail("Shouldn't reach here"); } catch (Exception e) { // NOOP } assertEquals("Should return bar", "bar", mock.methodReturnsString()); assertEquals("Should return 1", 1, mock.methodReturnsInt()); assertEquals("Should return 2", 2, mock.methodReturnsInt()); assertEquals("Should return 3", 3, mock.methodReturnsInt()); assertEquals("Should return foo", "foo", mock.methodReturnsString()); assertEquals("Should return bar", "bar", mock.methodReturnsString()); assertEquals("Should return 1", 1, mock.methodReturnsInt()); }

In this example, we have defined that for the first three calls to methodReturnsString() the expected returns are (in order) “foo”, an exception and “bar”. We achieved this using three different assignations to the result field.

Then on line 14, we defined that for the fourth and fifth calls, “foo” and “bar” should be returned using the returns(Object…) method.

For the methodReturnsInt() we defined on line 13 to return 1, 2 and lastly 3 by assigning an array with the different results to the result field and on line 15 we defined to return 1 by a simple assignation to the result field.

As you can see there are several ways of defining return values for mocked methods.

3.2. Delegators

To end the article we're going to cover the third way of defining the return value: the Delegate interface. This interface is used for defining more complex return values when defining mocked methods.

We're going to see an example to simply the explaining:

public interface ExpectationsCollaborator { int methodForDelegate(int i); } @Test public void testDelegate(@Mocked ExpectationsCollaborator mock) { new Expectations() {{ mock.methodForDelegate(anyInt); result = new Delegate() { int delegate(int i) throws Exception { if (i < 3) { return 5; } else { throw new Exception(); } } }; }}; assertEquals("Should return 5", 5, mock.methodForDelegate(1)); try { mock.methodForDelegate(3); fail("Shouldn't reach here"); } catch (Exception e) { } } 

The way to use a delegator is to create a new instance for it and assign it to a returns field. In this new instance, you should create a new method with the same parameters and return type than the mocked method (you can use any name for it). Inside this new method, use whatever implementation you want in order to return the desired value.

In the example, we did an implementation in which 5 should be returned when the value passed to the mocked method is less than 3 and an exception is thrown otherwise (note that we had to use times = 2; so that the second invocation is expected as we lost the default behaviour by defining a return value).

Cela peut sembler beaucoup de code, mais dans certains cas, ce sera le seul moyen d'obtenir le résultat souhaité.

4. Conclusion

Avec cela, nous avons pratiquement montré tout ce dont nous avons besoin pour créer des attentes et des vérifications pour nos tests quotidiens.

Nous publierons bien sûr plus d'articles sur JMockit, alors restez à l'écoute pour en savoir plus.

Et, comme toujours, l'implémentation complète de ce tutoriel peut être trouvée sur le projet GitHub.

4.1. Articles de la série

Tous les articles de la série:

  • JMockit 101
  • Un guide des attentes JMockit
  • Utilisation avancée de JMockit