Mocking en Java avec Mockito
Simulez des dépendances dans vos tests Java avec Mockito : mock, when/thenReturn, verify et ArgumentCaptor.
Un test unitaire doit exercer une seule classe en isolation. Mais les classes réelles s'appuient sur des collaborateurs — une base de données, une passerelle de paiement, un service d'envoi d'e-mails — qui sont lents, peu fiables ou ont des effets de bord indésirables dans un test. Mockito est la bibliothèque Java la plus utilisée pour remplacer ces collaborateurs par des mocks : des objets de substitution que vous programmez pour retourner des réponses prédéfinies et que vous interrogez ensuite sur la façon dont ils ont été appelés. Ce chapitre présente l'API Mockito que vous utiliserez au quotidien, et illustre l'idée sous-jacente avec un programme JDK pur que vous pouvez exécuter directement ici.
Ce chapitre suppose que vous connaissez déjà les bases des tests abordées dans Introduction à JUnit 5 et Assertions JUnit. Mockito complète JUnit — JUnit exécute le test et vérifie les valeurs, tandis que Mockito fournit les faux collaborateurs.
Pourquoi utiliser des mocks
La classe sous test (le système sous test, ou SUT) reçoit généralement ses collaborateurs via son constructeur — c'est le bénéfice de l'injection de dépendances. Dans un test, vous lui fournissez un faux collaborateur à la place du vrai. Un bon faux collaborateur remplit deux rôles :
- Stubbing — il retourne la valeur dont le scénario de test a besoin (
charge(...)retournetrue, ou lève une exception), vous permettant de guider le SUT vers un chemin spécifique sans effectuer de vrai appel réseau. - Vérification — il enregistre chaque appel reçu, de sorte que le test peut ensuite vérifier que le SUT l'a appelé de la bonne façon, le bon nombre de fois, avec les bons arguments.
Mockito génère un tel faux pour n'importe quelle interface ou classe non finale au moment de l'exécution, vous n'avez donc jamais à en écrire un manuellement. Mais comprendre ce qu'il génère rend l'API évidente.
Créer des mocks et stubber des retours
Mockito.mock(Type.class) produit un mock. Par défaut, chaque méthode retourne une valeur vide « agréable » — null pour les objets, false pour les booléens, 0 pour les nombres. Vous surchargez ensuite les méthodes qui vous intéressent avec when(...).thenReturn(...).
import static org.mockito.Mockito.*;
PaymentGateway gateway = mock(PaymentGateway.class);
// Stub: when charge is called with these args, return true.
when(gateway.charge("acct-7", 1999)).thenReturn(true);
// Stub a method to throw, to test error handling.
when(gateway.charge("acct-x", 1)).thenThrow(new GatewayException("down"));Pour les méthodes void, l'ordre s'inverse : doThrow(...).when(mock).method(). Les stubs peuvent également être assouplis avec des argument matchers comme anyString() et anyInt() afin de s'activer pour n'importe quel appel, et pas seulement pour un ensemble exact d'arguments.
Vérifier les interactions
Après l'exécution du SUT, verify(...) vérifie comment le mock a été utilisé. C'est ainsi que vous testez les effets de bord — un e-mail qui aurait dû être envoyé, une ligne qui aurait dû être sauvegardée — sans inspecter le vrai système.
verify(gateway).charge("acct-7", 1999); // called exactly once (default)
verify(gateway, times(2)).charge(anyString(), anyInt());
verify(gateway, never()).refund(anyString()); // must NOT have been called
verifyNoMoreInteractions(gateway); // nothing else happenedLes modes de vérification courants :
| Mode | Signification |
|---|---|
times(n) | Appelé exactement n fois |
never() | Identique à times(0) |
atLeastOnce() / atLeast(n) | Appelé au moins une fois / n fois |
atMost(n) | Appelé au maximum n fois |
only() | C'était la seule méthode appelée sur le mock |
Capturer des arguments
Lorsque vous devez inspecter ce qui a été passé — et pas seulement qu'un appel a eu lieu — utilisez un ArgumentCaptor. Il récupère l'argument réel afin que vous puissiez vérifier ses champs, ce qui est très utile lorsque le SUT construit un objet avant de le transmettre.
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
verify(repository).save(captor.capture());
Order saved = captor.getValue();
assertEquals("acct-7", saved.account());
assertEquals(1999, saved.amountCents());@Mock, @InjectMocks et les spies
Dans les classes de test réelles, vous appelez rarement mock() manuellement. Les annotations câblent tout : @Mock déclare un champ mock, @InjectMocks construit le SUT et injecte les mocks dans son constructeur, et @ExtendWith(MockitoExtension.class) (JUnit 5) active le traitement.
@ExtendWith(MockitoExtension.class)
class CheckoutServiceTest {
@Mock PaymentGateway gateway;
@InjectMocks CheckoutService service; // gets the mock injected
@Test
void paysWhenGatewayApproves() {
when(gateway.charge("acct-7", 1999)).thenReturn(true);
assertEquals("PAID", service.checkout("acct-7", 1999));
verify(gateway).charge("acct-7", 1999);
}
}Un spy (spy(realObject)) est un intermédiaire : il enveloppe un objet réel et exécute les vraies méthodes sauf si vous les stubbez — pratique pour le mocking partiel de code legacy.
final, les méthodes final, les méthodes static ou les méthodes private. Si vous devez mocker une classe final, activez le MockMaker mockito-inline ; sinon, refactorisez vers une interface.Quand ne pas utiliser les mocks
Les mocks sont puissants, mais un sur-mocking produit des tests qui passent alors que le vrai code est cassé. Utilisez un mock uniquement lorsque le vrai collaborateur est lent, non déterministe, a des effets de bord, ou n'est pas encore construit. Ne moquez pas les objets valeurs, la classe sous test elle-même, ni les types que vous ne possédez pas (encapsulez une API tierce dans votre propre interface et mockez celle-là). Lorsque le collaborateur est simple et pur — un calculateur basique, une liste en mémoire — utilisez le vrai et vérifiez directement son résultat.
Un exemple concret : un mock fait à la main
Mockito n'est pas disponible dans le classpath de cette page, donc le programme exécutable ci-dessous construit le mock à la main — une petite classe implémentant l'interface de dépendance qui contient une valeur de retour stubbée et enregistre chaque appel. C'est précisément la mécanique que Mockito génère pour vous à l'exécution, donc la lire vous indique exactement ce que font when/thenReturn et verify sous le capot.
Ce qu'il faut retenir de l'exécution :
- Le
stubbedResult = truedeMockGatewayest la forme écrite à la main dewhen(gateway.charge(...)).thenReturn(true); parce que le stub a retournétrue, le SUT a affichéresult : PAIDsans qu'aucun vrai paiement n'ait eu lieu. invocationCount == 1affichanttrueest exactement ce que vérifieverify(gateway).charge(...)— le mock a compté qu'il a été appelé une fois, ce qui est la façon dont Mockito transforme « cette interaction a-t-elle eu lieu ? » en assertion pass/fail.- La liste
callsa capturécharge(acct-7, 1999), l'idée de capture d'arguments derrièreArgumentCaptor: un mock se souvient non seulement qu'il a été appelé mais avec quoi, afin que le test puisse vérifier les arguments réels. - Recréer le mock avec
stubbedResult = falsea guidé le SUT dans son autre branche et affichédeclined result : DECLINED, montrant comment un faux permet de scénariser chaque situation que le vrai collaborateur pourrait produire. - La clause de garde a retourné
INVALIDavant d'atteindre la passerelle, doncinvocationCount == 0a affichétrue— la preuve exécutable deverify(gateway, never()).charge(...), affirmant qu'une dépendance n'a délibérément pas été touchée.