Mockito ou comment faciliter l’écriture de tests unitaires

MockitoMockito est un framework Java, permettant de mocker ou espionner des objets, simuler et vérifier des comportements, ou encore simplifier l’écriture de tests unitaires.

Pour ceux qui veulent (re)trouver du plaisir à écrire des TU, cet article est fait pour vous.

Mais attention… une fois qu’on commence à y prendre goût, difficile de s’arrêter !

Mais déjà qu’est-ce qu’un mock ? C’est un objet qui simule le comportement d’un objet réel.
Ces objets factices sont très souvent utilisés dans les tests.

Il peut être assez fastidieux d’initialiser le contexte de son test, et c’est sur cette partie que Mockito va nous aider.

Il s’inscrit très bien dans une approche Behavior Driven Development

En effet, il sera très intuitif d’écrire son test en suivant la notion //given //when //then, et nous verrons que Mockito met l’accent sur la 1ère et la 3ème notion.

Limitations

Avant de commencer, il est nécessaire de connaitre certaines limitations du framework :

  • Nécessite Java 1.5+
  • impossible de mocker une classe ou une méthode finale
  • impossible de mocker une méthode statique ou privée
  • impossible de mocker les méthodes equals() et hashcode() (Mockito redéfinit et dépend fortement de ces 2 dernières)

Préparation

Pour intégrer Mockito à son projet, rien de plus simple.

Télécharger une version ici et l’ajouter à son classpath.

Pour les utilisateurs de Maven, ajouter la dépendance :

<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>

Comment ça marche ?

Greffer Mockito sur une classe JUnit

2 possibilités :

  • Ajouter l’annotation @RunWith comme suivant :
@RunWith(MockitoJunitRunner.class)
public class MyTestClass {

}
  • Ou à l’initialisation dans la méthode setUp()
@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
}

Le stubbing

Mockito est capable de stubber des classes concrètes mais aussi des interfaces.

On peut appeler la méthode mock sur une classe :


User user = Mockito.mock(User.class);

Ou placer une annotation si la variable est en instance de classe


@Mock
User user;

public class MyTest { }

Voyons maintenant comment réagit un mock créé par Mockito.
Soit un objet User contenant 1 propriété login.


User user = Mockito.mock(User.class);
System.out.println(user.getLogin()); // affiche null
user.setLogin("bob");
System.out.println(user.getLogin()); // affiche encore null !

Dans cet exemple, on peut voir que Mockito encapsule et contrôle tous les appels effectués sur l’objet User.

On va alors pouvoir indiquer à Mockito quelle valeur on souhaite retourner lorsque la méthode getLogin() est invoquée.


User user = Mockito.mock(User.class);

Mockito.when(user.getLogin()).thenReturn("bob");

System.out.println(user.getLogin()); // affiche "bob"

user.setLogin("drake");
System.out.println(user.getLogin()); // affiche toujours "bob"

Nous n’avons pas stubbé la méthode setLogin, et Mockito ne connait qu’une seule réponse possible pour getLogin(). De ce fait, il ne renverra jamais “drake” tant qu’on ne lui aura pas dit.

Enfin, on peut rétablir le comportement normal d’une méthode :


User user = Mockito.mock(User.class);

// on rétablit le getLogin()
Mockito.when(user.getLogin()).thenCallRealMethod();

// affiche null mais cette fois car la variable login vaut vraiment null dans le mock
System.out.println(user.getLogin());

// on rétablit également le comportement de la méthode setLogin()
Mockito.doCallRealMethod().when(user).setLogin(Mockito.anyString());

user.setLogin("drake");
System.out.println(user.getLogin()); // affiche enfin "drake" !

Note : On peut apercevoir en argument de la méthode setLogin() ce qu’on appelle un Matcher.
Mockito.anyString() ne va pas générer une String quelconque mais va permettre de généraliser le comportement attendu (doCallRealMethod) quelle que soit la String passée en argument.

Si on avait voulu restreindre ce comportement, on aurait mis setLogin(“drake”) et uniquement la chaîne “drake” aurait permis cela.

Nous reviendrons sur les matchers juste après.

Passons maintenant au stubbing d’un objet réel, qui devient “espionné” par Mockito.

Tout comme le mock, le spy peut être déclaré de 2 façons :

On peut appeler la méthode spy sur un objet déjà instancié :


User user = Mockito.spy(new User());

Ou placer une annotation :


@Spy
User user = new User();

public class MyTest { }

Voici comment stubber uniquement une partie de notre objet


User user = Mockito.spy(new User());
Mockito.doReturn("drake").when(user).getLogin();

user.setLogin("bob");

// affiche "drake" mais la vraie valeur dans la classe User vaut bien "bob"
System.out.println(user.getLogin());

On peut aussi stubber une méthode void comme suivant :


Mockito.doNothing().when(user).getLogin(Mockito.anyString());

Enfin, on peut aussi choisir de générer une exception lors d’un appel :


Mockito.doThrow(new IllegalArgumentException()).when(user).setLogin(Mockito.eq("bad"));

La vérification

Passons maintenant à la façon dont Mockito vérifie qu’un appel a bien été effectué.

C’est la partie “then” de notre approche BDD.

L’idée est de dire : “Mockito, vérifie pour moi que la méthode m1() a été appelée une fois, avec exactement les arguments x et y”.

Ou encore “Mockito, vérifie pour moi que la méthode m2() n’a jamais été appelée avec un argument de type Long et dont la valeur est inférieure à 10”.

C’est donc ici que vont intervenir les fameux matchers.

Il existe plusieurs modes de vérifications. Par défaut un verify ne s’attend qu’à une seule exécution d’une méthode. S’il en existe plus dans le code testé, il faudra ajouter une clause supplémentaire à la vérification.

Voici quelques exemples :


// vérifie que la méthode m1 a été appelée sur obj, avec une String strictement égale à "bob"
Mockito.verify(obj).m1(Mockito.eq("bob"));
// note : ici, le matcher n'est pas indispensable, la ligne suivante est équivalente :
Mockito.verify(obj).m1("bob");

// vérifie que la méthode m1 a été appelée sur obj, avec un objet similaire à celui passé en argument
Mockito.verify(obj).m1(Mockito.refEq(obj2));

// vérifie que la méthode m2 n'a jamais été appelée sur l'objet obj
Mockito.verify(obj, Mockito.never()).m2();

// vérifie que la méthode m3 a été appelée exactement 2 fois sur l'objet obj
Mockito.verify(obj, Mockito.times(2)).m3();

// idem avec un nombre minimum et maximum
Mockito.verify(obj, Mockito.atLeast(3)).m3();
Mockito.verify(obj, Mockito.atMost(10)).m3();

Cette partie est très importante car elle prévient tout changement dans le code de production, et avertit donc en cas de régression lors de l’exécution des tests unitaires.

Passons maintenant aux matchers.

Nous avons vu brièvement Mockito.eq(), Mockito.refEq() et Mockito.anyString()

Mockito fournit une liste de méthodes retournant des matchers prédéfinis, qui suffisent en général à satisfaire nos attentes :

  • anyObject() : autorise un objet de n’importe quel type.
  • any(Class<T> clazz) : autorise tout objet du type spécifié
  • anyList() : autorise tout objet qui implémente List
  • anyFloat

La liste est longue, je vous invite à consulter la documentation.

Une contrainte est tout de même à respecter : si au moins un des arguments passés à la méthode verify() utilise un matcher, tous les autres le doivent également.


// incorrect !
Mockito.verify(list).m1(0, Mockito.eq("bob"));

Il est possible de définir ses propres matchers :


@Test
public void testArgMatcher() {
    ...
    // valide que la date passée en argument de setDate est bien non nulle et postérieure à la date du jour
    Mockito.verify(obj).setDate(Mockito.argThat(new MyDateMatcher()));
}

private class MyDateMatcher extends ArgumentMatcher<Date> {

    @Override
    public boolean matches(Object argument) {
        Date d = (Date) argument;
        return d != null && d.after(new Date());
    }
}

Dernière fonctionnalité qui mérite d’être présentée, la vérification sur l’ordre des instructions.

Mockito fournit un mécanisme permettant de valider qu’un enchaînement bien précis de méthodes a été réalisé sur un ou plusieurs objets :


User u1 = Mockito.mock(User.class);
User u2 = Mockito.mock(User.class);

u1.setLogin("bob");
u2.setLogin("drake");

// ajoute ces 2 mocks à l'ordre de vérification
InOrder inOrder = Mockito.inOrder(u1, u2);

// en inversant ces instructions, le test va échouer
inOrder.verify(u1).setLogin("bob");
inOrder.verify(u2).setLogin("drake");

Ce mode de vérification est plutôt flexible, il n’est pas nécessaire de vérifier toutes les méthodes appelées sur les objets monitorés.

Conclusion

Dans cet article, nous avons vu les principales fonctionnalités qu’offre ce merveilleux outil qu’est Mockito, pour la rédaction de tests unitaires. Malgré les quelques limitations énoncées, ses possibilités sont très vastes.

De quoi prendre un nouveau départ sur les tests unitaires ?

Bon testing !

© SOAT
Toute reproduction interdite sans autorisation de l’auteur

Pour aller plus loin

Nombre de vue : 14149

COMMENTAIRES 11 commentaires

  1. Jean dit :

    Merci beaucoup pour ton article sur un outil qui gagne à être utilisé systématiquement 🙂

  2. Matt dit :

    Très bon article. La notion de ArgumentCaptor très facile à mettre en oeuvre permet de “capturer” ce qui est passé en argument de méthodes.
    Je teste la méthode d’un service qui appelle la méthode d’un DAO. Je peux capturer ce qui est passé en paramètre de la méthode du DAO afin de vérifier les données.

  3. […] Mockito ou comment faciliter l’écriture de tests unitaires […]

  4. Alain dit :

    Bonjour,

    Merci pour cette découverte, cependant, j’aurais une petite question :

    Quel est la différence entre
    Mockito.when(user.getLogin()).thenReturn(“bob”);
    et
    Mockito.doReturn(“bob”).when(user).getLogin();
    ?

  5. Aurélien Garnier dit :

    Bonjour,

    Les 2 notations que tu compares seront acceptées pour un mock et produiront le même résultat.
    Il est préférable d’utiliser when() suivi de thenReturn() pour 2 raisons :
    – la lisibilté
    – la fiabilité du typage à la compilation : doReturn prend un Object alors que thenReturn connait le type du retour à renvoyer

    doReturn sera utile dans certains cas, je t’invite à consulter la documentation de la méthode qui explique bien cela.
    http://goo.gl/2rlM5

  6. […] de la syntaxe proposée. Dans ce sens, l’excellent JMockit ou encore le très populaire tandem Mockito / PowerMock me semblent de très bons […]

  7. Ralphy dit :

    merci beaucoup 🙂

  8. LOTFI dit :

    Bonjour,

    Je vois que l’article date du 2013, mais bon, je veux savoir qu’elle est la différence entre : Mock & Spy ?
    Et quand je dois opter à l’utilisation d’un des deux et pas l’autre ?

  9. mark dit :

    You can create spies of real objects. When you use the spy then the real methods are called (unless a method was stubbed).
    Real spies should be used carefully and occasionally, for example when dealing with legacy code.

    Spying on real objects is associated with “partial mocking” concept.

    List list = new LinkedList();
    List spy = spy(list);

    //optionally, you can stub out some methods:
    stub(spy.size()).toReturn(100);

    //using the spy calls real methods
    spy.add(“one”);
    spy.add(“two”);

    //prints “one” – the first element of a list
    System.out.println(spy.get(0));

    //size() method was stubbed – 100 is printed
    System.out.println(spy.size());

    //optionally, you can verify
    verify(spy).add(“one”);
    verify(spy).add(“two”);

AJOUTER UN COMMENTAIRE