# Mockito Quick Start Tutoriel
# Les concepts de base
Photo by Hessam Hojati on Unsplash
Mockito (opens new window) est une librairie qui permet de créer des tests doubles (opens new window) (mock, spy, stub) dans ses tests unitaires puis de vérifier leur comportement. Mockito s'intègre très bien avec les différents frameworks de tests unitaires et avec JUnit en particulier.
L'intérêt des tests doubles apparaît rapidement lorsque le comportement de la classe à tester est dépendant d'autres classes, que ce soit des services, des DAO ou autres fournisseurs de ressources. Dans ce cas, pour isoler le comportement de la classe à tester du comportement de ses dépendances, le plus simple est encore de les mocker: c'est à dire d'injecter comme dépendance des objets dont le comportement est contrôlé.
Mockito défini 3 types de tests doubles:
- Mock: les mocks sont créés directement à partir de la classe et le comportement de chaque méthode du mock peut être programmé ou non.
- Spy: les spy sont créés à partir d'un objet déjà existant. Le spy se comporte par défaut comme son objet, mais ce comportement peut être modifié au besoin.
- Partial Mock: les mocks partiels sont fortement déconseillés. Tous comme les mocks, ils sont créés à partir de la classe, mais il est possible de forcer l'exécution du code réel.
Pour chaque test double, Mockito offre plusieurs possibilités:
- Stubbing: les méthodes des mock ou des spy peuvent être reprogrammées pour choisir la valeur à retourner ou pour lever une exception en fonction des arguments qui sont passés lors de l'appel.
- Vérification des interactions: Mockito permet de vérifier si les méthodes d'un mock ou d'un spy ont été appelées, combien de fois, dans quel ordre et avec quels arguments.
# Installation avec Maven
Pour commencer, il faut inclure Mockito comme dépendance de test dans la configuration maven (pensez à vérifier la dernière version sur mvnrepository (opens new window)):
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.28.2</version>
<scope>test</scope>
</dependency>
</dependencies>
2
3
4
5
6
7
8
L'ensemble des fonctionnalités sont ensuite disponibles sous forme de méthodes statiques au niveau
de la classe org.mockito.Mockito
. Le plus simple est encore de les ajouter dans le test
via un import statique:
import static org.mockito.Mockito.*;
# Mocking
Pour créer un mock, il suffit d'utiliser Mockito.mock(SomeClass.class)
. Par défaut, les méthodes
de l'objet obtenu vont renvoyer une valeur: soit null
ou une valeur de primitive
(par exemple 0 pour les int/Integer, false pour les boolean/Boolean) ou une collection vide.
@Test
public void mock() {
Country country = Mockito.mock(Country.class);
assertThat(country.getName()).isNull();
}
2
3
4
# Spying
La création d'un spy est tout aussi simple, cette fois c'est la méthode Mockito.spy(someObject)
qui est utilisée. Le spy obtenu continu de garder le comportement de l'objet tant qu'aucun
stub n'a été défini.
@Test
public void spy() {
Country spy = Mockito.spy(Country.USA);
assertThat(spy.getName()).isEqualTo("United States of America");
}
2
3
4
# Stubbing
Le stubbing permet de définir le comportement attendu sur les méthodes d'un mock ou d'un spy. Pour cela,
on utilise Mockito.when(someMockedObject.methodToStub())
:
@Test
public void stubbing() {
Country country = Mockito.mock(Country.class);
when(country.getName()).thenReturn("France");
assertThat(country.getName()).isEqualTo("France");
when(country.getName()).thenAnswer(invocationOnMock -> "France");
assertThat(country.getName()).isEqualTo("France");
when(country.getName()).thenThrow(new NullPointerException());
assertThrows(NullPointerException.class, () -> country.getName());
}
2
3
4
5
6
7
8
9
La méthode thenReturn
défini directement la valeur à retourner lorsque le stub
est appelé. La méthode thenAnswer
, en revanche, défini dynamiquement cette valeur en fonction du contexte
(par exemple les arguments; ce context étant passé via un objet de la classe org.mockito.invocation.InvocationOnMock
).
TIP
Dans l'exemple ci-dessus, la méthode getName
a été reprogrammée plusieurs fois. Dans ces cas,
c'est toujours le dernier stub qui prends le pas sur les précédents.
# Correspondance des arguments
Il est également possible de restreindre un stub en fonction des arguments passés lors de l'appel. Cela
permet de rendre le test plus explicite en indiquant comment le stub est utilisé. Mockito introduit la
notion d'ArgumentMatcher
pour valider dynamiquement si un stub s'applique ou non suivant les arguments
passés en paramètre.
@Test
public void stubbingWithArgumentMatcher() {
Countries countries = Mockito.spy(new Countries());
when(countries.contains(Country.USA)).thenReturn(true);
assertThat(countries.contains(Country.USA)).isTrue();
assertThat(countries.contains(Country.FRANCE)).isFalse();
reset(countries);
when(countries.contains(any(Country.class))).thenReturn(true);
assertThat(countries.contains(Country.USA)).isTrue();
assertThat(countries.contains(Country.FRANCE)).isTrue();
reset(countries);
ArgumentMatcher<Country> match = country -> country.getDensity() >= Country.FRANCE.getDensity();
when(countries.contains(argThat(match))).thenReturn(true);
assertThat(countries.contains(Country.USA)).isFalse();
assertThat(countries.contains(Country.FRANCE)).isTrue();
assertThat(countries.contains(Country.CHINA)).isTrue();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TIP
La méthode statique Mockito.reset()
permet de réinitialiser un mock ou un spy dans son état initial.
Dans l'exemple ci-dessus, cette méthode est utilisé pour changer le comportement du mock au cours du test
en repartant de zéro.
# Appels successifs
Lorsqu'un stub a vocation à être appelé plusieurs fois, il est possible de changer la valeur retournée
à chaque appel. Soit en spécifiant plusieurs valeurs directement dans la méthode thenReturn
, soit en chaînant
les thenReturn
, thenAnswer
et thenThrow
:
@Test
public void stubbingSuccessiveCalls() {
Countries countries = Mockito.spy(new Countries());
when(countries.contains(Country.USA)).thenReturn(true, false, true);
assertThat(countries.contains(Country.USA)).isTrue();
assertThat(countries.contains(Country.USA)).isFalse();
assertThat(countries.contains(Country.USA)).isTrue();
when(countries.contains(Country.USA)).thenReturn(true).thenThrow(new NullPointerException());
assertThat(countries.contains(Country.USA)).isTrue();
assertThrows(NullPointerException.class, () -> countries.contains(Country.USA));
}
2
3
4
5
6
7
8
9
10
Si le stub est appelé plus que le nombre de valeur de retour défini, la dernière valeur définie est utilisée pour les appels suivants.
WARNING
Pour définir le comportement lors d'appels successifs, il ne faut pas redéfinir le stub complétement
(en repassant par when
) puisque lorsque le stub est redéfini, le comportement précédent est écrasé.
# Methode (void)
Il est également possible de créer un stub sur les méthodes void:
soit pour indiquer à un spy de ne rien faire (doNothing()
) lorsqu'une de ces méthodes est appelée
(plutôt que d'appeler le code réel), soit pour demander de lever une exception doThrow()
:
@Test
public void stubbingVoidMethod() {
Countries countries = Mockito.spy(new Countries());
doNothing().when(countries).add(Country.USA);
countries.add(Country.FRANCE);
countries.add(Country.USA);
assertThat(countries.size()).isEqualTo(1);
doThrow(new NullPointerException()).when(countries).add(Country.USA);
assertThrows(NullPointerException.class, () -> countries.add(Country.USA));
}
2
3
4
5
6
7
8
9
# Verification
Les mock et les spy permettent de contrôler les dépendances injectées dans la classe testée. Comme le code exécuté est factice, il est intéressant de vérifier si et comment la classe interagit avec ces dépendances. Par exemple, pour vérifier qu'un service appelle bien la méthode de mise à jour des données d'un DAO mocké et ce avec le bon argument.
# Verifier le nombre d'appel
Mockito.verify
vérifie que le stub d'un mock ou un spy est appelé (ou non). Mockito offre différent
helper pour s'assurer du nombre d'interaction, de manière fixe ou bornée:
@Test
public void verifyInteraction() {
Country spy = Mockito.spy(Country.USA);
verify(spy, never()).getName();
assertThat(spy.getName()).isEqualTo("United States of America");
verify(spy).getName();
verify(spy, atMostOnce()).getName();
verify(spy, atLeastOnce()).getName();
when(spy.getName()).thenReturn("France");
assertThat(spy.getName()).isEqualTo("France");
verify(spy, times(2)).getName();
verify(spy, atLeast(1)).getName();
verify(spy, atMost(3)).getName();
verify(spy, never()).getPopulation();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# Verifier les arguments utilisés lors de l'appel
De la même manière que les ArgumentMatcher
sont utilisés pour controller l'execution d'un stub,
ils peuvent être utilisés pour vérifier ses interactions. Là encore, il s'agit d'expliciter
dans le test comment le mock est utilisé:
@Test
public void verifyWithArgumentMatcher() {
Countries countries = Mockito.spy(new Countries());
assertThat(countries.contains(Country.FRANCE)).isFalse();
ArgumentMatcher<Country> matcher =
country -> country.getDensity() >= Country.FRANCE.getDensity();
verify(countries).contains(argThat(matcher));
}
2
3
4
5
6
7
# Vérifier en capturant les arguments
En plus de vérifier comment le mock a interagit, il est possible, lors de cette vérification,
de capturer les arguments reçus par le mock au moment de l'appel. Mockito utilise le concept
d'ArgumentCaptor
pour récupérer ces arguments. Une fois capturés, il devient possible de les tester:
@Test
public void verifyWithArgumentCaptor() {
Countries countries = Mockito.spy(new Countries());
assertThat(countries.contains(Country.FRANCE)).isFalse();
ArgumentCaptor<Country> argument = ArgumentCaptor.forClass(Country.class);
verify(countries).contains(argument.capture());
assertThat(argument.getValue().getName()).isEqualTo("France");
}
2
3
4
5
6
7
# Vérification à la mode Behavior Driven Development
Mockito fournit également des helpers orientés BDD via la classe org.mockito.BDDMockito
. Le principal
changement est le nom des méthodes qui offrent une sémantique de type given, when, then:
@Test
public void verifyBdd() {
Country mock = Mockito.mock(Country.class);
given(mock.getName()).willReturn("France");
assertThat(mock.getName()).isEqualTo("France");
then(mock).should(times(1)).getName();
}
2
3
4
5
6
# Autres fonctionnalités
Mockito propose d'autres fonctionnalités qui ne sont pas exposées dans ce tuto:
un jeu d'annotations (@Mock
, @Spy
, @Captor
, @InjectMocks
), la vérification de
l'ordre des interactions (inOrder()
),
le mock de classe abstraite,
le mock de classes et méthodes finales
ou encore le mock Partiel.
RTFM (opens new window)
en buvant des mojitos (opens new window) 😄