Mockito pour les débutants

Ce document est destiné aux débutants en tests unitaires. L’objectif, est de vous expliquer d’abord ce qu’est un test unitaire puis, comment en implémenter facilement à l’aide de Mockito.
Pour comprendre ce document, vous devez avoir des connaissances basiques en Java.

Les principes

Imaginez que votre application soit un vélo. Votre rôle dans l’équipe, est de développer la roue arrière du vélo. Votre roue doit avoir plusieurs caractéristiques : être ronde, suffisamment solide pour supporter le poids du cycliste, avec un pneu gonflé, être reliée au plateau pour tourner… Bien. Vous avez fini votre développement. A présent, le chef de projet décide d’ajouter une motorisation électrique sur le vélo. Cette modification va entraîner des modifications de votre roue, mais, il est important qu’elle conserve les caractéristiques de base d’une roue de vélo ! Vous revenez quelques jours plus tard et le moteur a été ajouté. La roue est toujours ronde, peut tourner et l’action des pédales entraîne sa rotation. Le vélo est commercialisé et se vend à plusieurs milliers d’unités. Là, vous apprenez que le vélo manque de solidité et que la roue arrière s’écrase au moindre choc… Lors de votre phase de développement, vous avez oublié de vérifier que votre roue était toujours suffisamment solide ! L’ajout de la motorisation a entraîné une régression.

Comment éviter que cela arrive ? Nous pourrions définir un ensemble de tâches à accomplir à chaque modification afin d’être certain que notre vélo continue de fonctionner comme avant mais en mieux. Cependant, notre temps est précieux et faire du vélo toute la journée en prenant le risque qu’il ne résiste pas à la course ne m’intéresse pas. Une machine pourrait faire l’affaire ! Installer un robot sur le vélo et le faire pédaler sur un circuit pendant des heures afin d’être sûr que tout fonctionne ? C’est pas mal … Mais si on avait une méthode permettant de valider que chaque composant de votre vélo fonctionne selon les règles fixées ? Par exemple, décrire toutes les caractéristiques désirées pour notre roue et avoir une machine effectuant ces tests très rapidement… Félicitations, nous avons découvert le principe du test unitaire.

Les problématiques

Maintenant que l'intérêt des tests unitaires est plus clair, et que vous avez trouvé une super solution pour éviter toute régression sur votre produit, il est temps de mettre de côté notre analogie afin de se recentrer sur notre domaine de compétences : les logiciels. Développer des tests unitaires vous permettra de diminuer le risque de bugs sur votre application. Cependant, ils doivent répondre à plusieurs principes connus sous l'acronyme FIRST. C’est-à-dire que chacun de ces tests doit être :

  • Foudroyant (rapide) : S’exécute rapidement et est donc automatisé
  • Isolé : Est indépendant des facteurs externes et des autres tests
  • Répétable : Isole les bugs automatiquement
  • Souverain (autonome) : N’est pas ambigu (pas sujet à interprétation, ne demande pas une action manuelle pour vérifier le résultat)
  • Tôt : Écrit en même temps que le code (même avant lorsque l’on fait du Test Driven Development)

Donc, vous ne devez pas avoir besoin de démarrer l’application pour vérifier le bon fonctionnement de celle-ci. On soulève ici un des principaux problèmes des tests : l’isolation. Comment déterminer le comportement de vos DAOs (Data access object) ou de vos services sans démarrer l’application ? C’est là qu’intervient Mockito.

Mockito

Mockito s’utilise conjointement à JUnit. JUnit permet de créer les tests unitaires du code applicatif. Mockito quant à lui va nous permettre de simuler les comportements des dépendances afin d’isoler nos tests. Je parlerai ici principalement de Mockito. Vous retrouverez l’utilisation de JUnit et de Mockito dans le projet lié à cet article dans la partie “Application”. Il permet de simplifier l’écriture de tests unitaires en fournissant un ensemble d'éléments que nous verrons plus tard. Si vous utilisez Maven, son installation est assez simple. Ajoutez ceci à votre fichier pom.xml :

<dependency>

<groupId>org.mockito</groupId>

<artifactId>mockito-core</artifactId>

<version>3.3.3</version>

<dependency>

Bien. Nous avons tout à l’heure évoqué le problème de l’isolement des tests. En effet, récupérer des données d’une base sans démarrer l’application semble complexe et ce n’est pas souhaitable, du point de vue des performances mais surtout dans un souci de maîtrise du contexte de test (les données ne doivent pas changer). C’est là que Mockito intervient à l’aide de ce qu’on appelle un mock. Un mock est un objet simulé qui reproduit le comportement d'objets réels de manière contrôlée. Voilà deux manières de le déclarer :

@Mock
private TodoRepository todoRepository

Ou bien :

private TodoRepository todoRepository = mock(TodoRepository.class)

L’utilisation de l’annotation oblige à passer par un initMocks() présent dans une méthode setup() annotée par @Before. Vous pouvez également annoter votre classe comme suit :

@ExtendedWith(MockitoExtension.class)
public class myTestClass {}

Maintenant que votre repository est “mocké”, il vous suffit de définir à l’avance le retour de chaque méthode qu’il possède à l’aide de la méthode when().

Imaginons que notre objet todoRepository possède une méthode getAll() qui retourne tous les todos présents dans une base de données. Il nous suffirait de créer une collection d’objets appelée myListOfTodo puis d’écrire cette ligne :

when(todoRepository.getAll()).thenReturn(myListOfTodo);

Désormais, lors du test, tout appel à la méthode getAll() retournera myListOfTodo sans que la base ne soit réellement interrogée. Nous avons simulé un comportement afin de permettre à nos tests de fonctionner dans un cas précis.

Nous allons désormais tester une méthode qui prend un paramètre en entrée.

Imaginons la méthode findATodo(String toFind). Cette méthode a pour rôle de rechercher un todo dans la base grâce au nom de sa tâche. Elle retourne : soit le todo s'il est trouvé, soit null s'il n'est pas présent en base. Nous allons donc passer par la méthode when(), comme avant, mais, en précisant ce qu'on appelle un matcher.

when(todoRepository.findATodo(anyString()).thenReturn(new Todo());

Le matcher anyString() indique que cette méthode doit retourner un newTodo() tant qu'elle est appelée avec un paramètre de type String. La valeur de cette String ne changera pas le résultat.

Des matchers similaires existent tels que : any(Object.class) (N'importe quel objet), anyInt() (N'importe quel entier) ou eq() (Une valeur précise).

Nous avons donc vu comment vérifier le retour de nos méthodes afin de les tester. Mettons une méthode permettant de démarrer un batch à une heure précise. Cette méthode ne retournerait pas d'élément, serait présente dans la classe Service et serait typée comme suit :

public void runBatch() {}

En observant cette méthode, on se rend bien compte que la tester comme les autres n’est pas possible. Pour pallier ce problème, nous utiliserons la méthode verify.

verify(mockedObject, times(x)).someMethod();

Après avoir “mocké” la classe Service, vérifiez que notre méthode a été appelée une fois :

verify(mockedService, times(1)).runBatch();

Conclusion

Tester votre application est essentiel pour permettre sa maintenabilité et son évolutivité. De plus, l’intégration de tests unitaires vous obligera à développer un code propre. L’écriture de tests unitaires peut ralentir la livraison de vos développements dans un premier temps mais, avec l'expérience, le temps d’intégration sera plus faible.

Application

En suivant ce lien, vous trouverez un mini projet commenté pour aller plus loin dans les tests et comprendre en direct comment les tests fonctionnent.

https://github.com/LofoWalker/MockitoBootcamp

Sources

https://site.mockito.org/
https://junit.org/junit5/