Introduction
L'écriture des tests unitaires ou d'intégration est une tâche cruciale et délicate en génie logiciel mais parfois fastidieuse. Elle nécessite une bonne connaissance des librairies et frameworks de tests et une bonne expérience en développement afin d'écrire des tests propres, maintenables dans le temps et qui serviront de documentation vivante pour les nouveaux arrivants sur le projet.
En Java comme dans d'autres langages, il existe une panoplie de librairies et de frameworks de tests et nous allons aborder la librairie Mockito en JUnit et les bonnes pratiques à suivre afin de bien s'en servir.
Mockito - Définition
C'est une librairie permettant de mettre en place des substitutions aux objets (mock object), généralement coûteux à instancier, dans les tests unitaires (TU). Sa puissance réside dans sa capacité à définir et simuler les comportements souhaités pour ensuite vérifier que ces interactions attendues sont belle et bien réalisées.
Mockito permet également de créer un remplaçant pour uniquement vérifier son comportement sans le besoin à le définir ou partiellement le définir. Ils appellent cette fonctionnalité de l'espionnage (spying) en créant un objet d'espionnage (Spy object) dans le cadre d'une substitution partielle (partial mock).
Etapes du mock
Il y a quatre (4) étapes pour mocker en Mockito :
- Instancier un mock.
- Définir ses comportements : nous appelons cette action du stubbing (heurter en français).
- Réaliser les vérifications nécessaires.
- Accessoirement reset les mocks.
Vocabulaire utilisé pour désigner un substituant
Il y a un superbe article de Martin Fowler présentant les nuances concernant le terme Mock Object (object de mock) et qui cite ces cinq (5) types :
- Dummy objets bidon qui servent uniquement à remplir la liste des paramètres.
- Fake objets avec des implémentations partielles permettant de rendre un service et qui prend des raccourcis sur l'implémentation comme les bases de données en mémoire.
- Stubs fournissent des réponses préprogrammées à certains comportements uniquement.
- Spies substituants avec un enregistrement des appels qu'ils ont reçus.
- Mocks représentent les objets principaux des mocks, ils sont pré-programmés à certains comportements représentant des specifications de comments ils sont censés interagir avec leur environnement.
Que doit-on mocker
Lors d'un TU nous nous concentrons uniquement sur l'entité faisant l'objet de test principalement une classe que nous testons System Under Test (SUT). Par conséquent, pour y arriver, nous avons besoin de l'isoler du monde extérieur et donc de ses dépendances en les mockants ou bouchonnants.
En effet, il est préférable de ne mocker que des objets définissant des comportements et généralement coûteux à instancier à cause de leurs dépendances avec d'autres objets de l'application, ils sont indisponibles, ils réalisent des opérations qui sont en dehors de notre cas de test, ils sont lents ou génèrent des résultats random (dans le nut de maîtriser le résultat). Parmi ce type d'objets, nous pouvons retrouver : les services, les stores, les objets d'accès au réseau ou au système externe, etc.
Ne jamais bouchonner un objet de données comme un objet du domaine, DTO ou une entité DAO.
Il y a quelques objets complexes surtout dans le cadre de certains frameworks comme Spring. Ils sont souvent des contrats de réponse encapsulant un état complexe. D'habitude Spring fournit des adaptateurs à utiliser, sinon, il faudrait créer un adaptateur et/ou un builder pour faciliter leurs utilisations.
Utiliser l'extension
Mockito met à disposition des développeurs Java utilisant JUnit5 ou JUnit6 une extension MockitoExtension (l'équivalent de MockitoJUnitRunner pour les vintages de JUnit4) permettant d'initialiser les mocks, spies et les injecter dans l'objet SUT facilement avec uniquement des annotations ce qui rend le TU plus net avec moins de code.
Cette extension permet également un stubbing stricte c-à-d qu'elle va faire échouer le test si au moins un stubbing non nécessaire est configuré. C'est une très bonne chose car souvent ça mérite l'attention du développeur qui a mal estimé les comportements attendus ou un oublie lors d'un refactor de test. Ce mode stricte peut être allégé au besoin avec lenient.
Utiliser les annotations pour déclarer les mocks
Dans le but de simplifier l'initialisation et l'injection des mocks, il est préférable d'utiliser les annotations fournis pas Mockito. Il est également préférable de les utiliser avec l'extension ci-dessus qui permet de les parser et de les exploiter. Les annotations sont les suivantes (voici un article intéressant discutant les annotations Spring dont celles de Mockito en Spring):
- @Mock permet de déclarer et initialiser un mock.
- @Spy permet de déclarer un spy (une initialisation de l'objet à surveiller est nécessaire).
- @InjectMocks permet de déclarer le STU en lui injectant les mocks instanciés via les annotations précédentes.
Multiple injections n'est pas possible avec @InjectMocks
Si nous avons besoin d'injecter un mock à plusieurs endroits, nous sommes obligés de passer par l'initialisation à la main via Mockito.mock et Mockito.spy qui sont les équivalent des annotations de même nom ci-dessus.
Utiliser verify et ses dérivés
Après les appels aux mocks, il est important de faire les vérifications avec Mockito.verify sur tous les mocks utilisés et s'assurer que les comportements attendus sont belle et bien conformes aux spécifications prédéfinies lors de la phase de stubbing.
Il y a également les dérivées intéressantes à verify qu'il faut aussi utiliser pour s'assurer qu'aucun autre comportement inattendu n'est déclenché en plus sur un mock et/ou aucune interaction n'a été enregistrée avec le mock en question en utilisant respectivement Mockito.verifyNoMoreInteractions et Mockito.verifyNoInteractions.
Utiliser les vraies valeurs attendues
Lorsque nous définissant les comportements (stubbing) et les vérifications, il est préférable dans la mesure du possible d'utiliser les vraies valeurs attendues en entrée et en sortie afin de s'assurer du vrai comportement de notre SUT en lieu et place des matchers génériques.
Eviter l'utilisation des matchers Mockito.any et ses dérivés : Mockito.anyInt, Mockito.anyFloat, Mockito.anySet, etc. dans le stubbing et les vérifications.
Exemple d'utilisation de Mockito
Mockito en pratique
@ExtendsWith(Mockitoextension.class) // 1. utilisation de l'extension
class PersonneServiceTest {
@Mock // 2. déclaration de mock
PersonneStore personneStore;
@InjectMocks // 3. injection de mocks
PersonneService personneService;
@Test
void add() {
final Personne personToAdd = completePerson(); // 4. helper
final Personne personAdded = completePerson(); // 4. helper
final PersonneEntity personEntityToAdd = completePersonEntity(); // 4. helper
final PersonneEntity personEntityAdded = completeAddedPersonEntity(); // 4. helper
// 5. stubbing when(personneStore.add(personEntityToAdd)).thenReturn(personEntityAdded);
// 6. appel du STU
final Personne personAdded = personneService.add(personToAdd);
// 7. assertion
assertThat(personAdded)
.usingRecursiveComparison()
.isEqualTo(personAdded);
// 8. vérifications
verify(personneStore).add(personEntityToAdd);
verifyNoMoreInteractions(personneStore);
}
}
J'ai ajouté les numérotations dans le code en commentaire pour identifier les étapes pour tester un service de personne :
- Utiliser l'annotation MockitoExtension avec JUnit5 ou JUnit6.
- Déclaration de la dépendance PersonneStore comme mock avec @Mock.
- Déclaration du service à tester PersonneService en spécifiant qu'il a besoin qu'on lui injecte les mocks précédemment déclarés.
- Initialisation des données Fixtures en recourant à des helpers réutilisables.
- Stubbing du mock PersonneStore avec les valeurs attendues en entrée du service add et la valeur en retour (sortie).
- Appel réel du service à tester PersonneService.add.
- Assertion en AssertJ (librairie java pour les fluent assertions) pour vérifier le bon résultat de retour du service.
- Réaliser les vérifications sur le mock PersonneStore afin de s'assurer qu'il n'y a que les comportements souhaités qui sont utilisés.
Conclusion
Tout au long de cet article, nous avons exploré quelques bonnes pratiques à suivre lorsque nous substituants certains objets dans nos tests TU et en particulier en utilisant la librairie Mockito de Java.
Nous avons définit ce qu'est un mock et les différentes déclinaisons qu'il pourrait avoir. Ensuite, nous avons identifié les objets à mocker et par conséquent ceux qu'il faut impérativement éviter de mocker.
Enfin, nous avons évoqué Mockito avec son extension MockitoExtension à utiliser avec JUnit5/JUnit6 et les différentes annotations que nous pouvons utiliser pour alléger le test et le rendre plus lisible à savoir @Mock, @Spy et @InjectMocks. De plus, nous avons insisté sur l'utilisation de vraies valeurs lors de stubbing avec Mockito.when et lors des vérifications avec Mockito.verify et ses dérivés Mockito.verifyNoMoreInteractions et Mockito.verifyNoInteractions.
Autres articles à lire :