Accueil Nos publications Blog Mockito ou comment faciliter l’écriture de tests unitaires

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