Un guide sur les attentes de JMockit

1. Intro

Cet article est le deuxième article de la série JMockit. Vous voudrez peut-être lire le lien:/jmockit-101[premier article]car nous supposons que vous connaissez déjà 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 méthodes plus avancées de définition de valeurs.

2. Correspondance des valeurs d’argument

Les approches suivantes s’appliquent aussi bien à Expectations qu’à Verifications .

2.1. “Tout” champs

JMockit propose un ensemble de champs d’utilitaires permettant de rendre la correspondance des arguments plus générique. Un de ces utilitaires sont les champs anyX .

Ceux-ci vérifieront que toutes les valeurs ont été passées et qu’il y en a une pour chaque type de primitive (et la classe d’encapsuleur 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<String> 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<String>) 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 .

Celles-ci permettent une correspondance un peu plus avancée que les champs anyX .

Nous pouvons voir ici un exemple dans lequel nous allons définir une attente pour une méthode qui sera déclenchée avec une chaîne contenant foo , un entier différent de 1, un Boolean non nul et toute instance de la classe List :

public interface ExpectationsCollaborator {
    String methodForWith1(String s, int i);
    void methodForWith2(Boolean b, List<String> 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 méthodes withX sur documentation .

Tenez compte du fait que les options spéciales with (délégué) et withArgThat (Matcher) seront couvertes dans leur propre sous-section.

2.3. Null Is Not Null

Ce qu’il est bon de comprendre plus tôt que tard est que null n’est pas utilisé pour définir un argument pour lequel null a été passé à un mock.

En fait, null est utilisé en tant que sucre syntaxique pour définir le passage de tout objet (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’une simulation, à déclencher lorsque les arguments transmis sont: toute chaîne, toute liste et une référence null :

public interface ExpectationsCollaborator {
    String methodForNulls1(String s, List<String> l);
    void methodForNulls2(String s, List<String> l);
}

@Test
public void testWithNulls(@Mocked ExpectationsCollaborator mock){
    new Expectations() {{
        mock.methodForNulls1(anyString, null);
        result = "null";
    }};

    assertEquals("null", mock.methodForNulls1("blablabla", new ArrayList<String>()));
    mock.methodForNulls2("blablabla", null);

    new Verifications() {{
        mock.methodForNulls2(anyString, (List<String>) withNull());
    }};
}

Notez la différence: null signifie toute liste et withNull () signifie une référence null à une liste. Cela évite notamment de convertir la valeur en type de paramètre déclaré (voir que le troisième argument doit être converti, mais pas le second).

La seule condition pour pouvoir utiliser ceci est qu’au moins un correcteur d’arguments explicite ait été utilisé pour l’attente (une méthode with ou un champ any ).

2.4. Champ «Temps»

Parfois, nous voulons contraindre 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 autorisent uniquement 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 exactement deux invocations (pas une, pas trois, exactement deux) de methodForTimes1 () à l’aide de la ligne times = 2; .

Nous avons ensuite utilisé le comportement par défaut (si aucune contrainte de répétition n’est donnée, minTimes = 1; est utilisé) pour définir qu’au moins une invocation sera faite à methodForTimes2 () .

Enfin, en utilisant minTimes = 1; suivi de maxTimes = 3; , nous avons défini qu’entre un et trois invocations auraient lieu à 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. D’autre part, times ne peut être utilisé seul.

2.5. Correspondance d’argument personnalisé

Parfois, la correspondance d’arguments n’est pas aussi directe que de simplement spécifier une valeur ou d’utiliser certains des utilitaires prédéfinis ( anyX ou withX ).

Pour cela, JMockit s’appuie sur l’interface Hamcrest ’s Matcher Il vous suffit de définir un assortiment pour le scénario de test spécifique et de l’utiliser avec un appel withArgThat () .

Voyons un exemple de correspondance d’une classe spécifique avec 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<Object>() {
            @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 de retour

Voyons maintenant les valeurs de retour; Gardez à l’esprit que les approches suivantes s’appliquent uniquement à Expectations car aucune valeur de retour ne peut être définie pour Verifications .

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 allons parler maintenant des deux premiers (les plus simples) qui couvriront sûrement 90% des cas d’utilisation quotidiens.

Ces deux sont le champ result et la méthode returns (Object …​) :

  • Avec le champ result , vous pouvez définir une valeur de retour pour tout

méthode de retour non vide moquée. Cette valeur de retour peut également être une exception à lever (cette fois, elle fonctionne pour les méthodes de retour non nul et vide).

  • ** Plusieurs attributions de résultats peuvent être effectuées pour renvoyer

  • plusieurs valeurs ** pour plusieurs invocations de méthodes (vous pouvez combiner les valeurs de retour et les erreurs à générer).

  • ** Le même comportement sera obtenu lors de l’attribution à une liste de résultats

ou un tableau de valeurs (du même type que le type de retour de la méthode simulée, NO exceptions ici).

  • La méthode returns (Object …​) est sucre syntaxique pour retourner

plusieurs valeurs du même temps.

Ceci est plus facilement montré avec un extrait de code:

public interface ExpectationsCollaborator{
    String methodReturnsString();
    int methodReturnsInt();
}

@Test
public void testResultAndReturns(@Mocked Foo mock){
    new StrictExpectations() {{
        mock.methodReturnsString();
        result = "foo";
        result = new Exception();
        result = "bar";
        mock.methodReturnsInt(); result = new int[]{ 1, 2, 3 };
        mock.methodReturnsString(); returns("foo", "bar");
        mock.methodReturnsInt(); result = 1;
    }};

    assertEquals("Should return foo", "foo", mock.methodReturnsString());
    try {
        mock.methodReturnsString();
    } catch (Exception e) { }

    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());
}

Dans cet exemple, nous avons défini que pour les trois premiers appels à methodReturnsString () , les retours attendus sont (dans l’ordre) «foo» , une exception et «bar» . Nous avons atteint cet objectif en utilisant trois assignations différentes au champ result .

Puis, sur la ligne 14 , nous avons défini que pour les quatrième et cinquième appels, «foo» et «bar» devraient être renvoyés à l’aide de la méthode returns (Object …​) .

Pour methodReturnsInt () , nous avons défini sur la ligne 13 renvoyer 1, 2 et enfin 3 en affectant un tableau avec les différents résultats au champ result et sur la ligne 15 , champ__résultat

Comme vous pouvez le constater, il existe plusieurs façons de définir les valeurs de retour pour les méthodes simulées.

3.2. Délégateurs

Pour terminer l’article, nous allons aborder la troisième façon de définir la valeur de retour: l’interface Delegate . Cette interface est utilisée pour définir des valeurs de retour plus complexes lors de la définition de méthodes simulées.

Nous allons voir un exemple pour simplement expliquer:

public interface ExpectationsCollaborator {
    Object methodForDelegate(int i);
}

@Test
public void testDelegate(@Mocked ExpectationsCollaborator mock) {
    new Expectations() {{

        mock.methodForDelegate(anyInt);
        result = new Delegate() {
            public 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);
    } catch (Exception e) { }
}

Pour utiliser un délégant, vous devez créer une nouvelle instance et l’assigner à un champ returns . Dans cette nouvelle instance, vous devez créer une nouvelle méthode avec les mêmes paramètres et le même type de retour que la méthode fausse (vous pouvez utiliser n’importe quel nom pour cette méthode). Dans cette nouvelle méthode, utilisez l’implémentation de votre choix pour renvoyer la valeur souhaitée.

Dans l’exemple, nous avons réalisé une implémentation dans laquelle 5 devrait être renvoyé lorsque la valeur transmise à la méthode mocked est inférieure à 3 et qu’une exception est renvoyée (notez que nous avons dû utiliser times = 2; pour que le second appel soit attendu car nous avons perdu le comportement par défaut en définissant une valeur de retour).

Cela peut sembler beaucoup de code, mais dans les mêmes cas, ce sera le seul moyen d’obtenir le résultat souhaité.

4. Conclusion

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

Bien sûr, nous publierons plus d’articles sur JMockit, alors restez à l’écoute pour en apprendre davantage.

Et, comme toujours, la mise en œuvre complète de ce tutoriel est disponible à l’adresse le projet GitHub .

4.1. Articles de la série

Tous les articles de la série:

  • lien:/jmockit-101[JMockit 101]

  • lien:/jmockit-attentes[Un guide des attentes JMockit]