W3docs

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(...) retourne true, 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 happened

Les modes de vérification courants :

ModeSignification
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.

Avertissement
Mockito ne peut mocker que ce qui est surchargeable. Par défaut, il ne peut pas mocker les classes 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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • Le stubbedResult = true de MockGateway est la forme écrite à la main de when(gateway.charge(...)).thenReturn(true) ; parce que le stub a retourné true, le SUT a affiché result : PAID sans qu'aucun vrai paiement n'ait eu lieu.
  • invocationCount == 1 affichant true est exactement ce que vérifie verify(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 calls a capturé charge(acct-7, 1999), l'idée de capture d'arguments derrière ArgumentCaptor : 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 = false a 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é INVALID avant d'atteindre la passerelle, donc invocationCount == 0 a affiché true — la preuve exécutable de verify(gateway, never()).charge(...), affirmant qu'une dépendance n'a délibérément pas été touchée.

Exercice

Pratique
Dans un test unitaire basé sur Mockito, quel est le rôle d'un appel comme verify(gateway, never()).charge(anyString(), anyInt()) ?
Dans un test unitaire basé sur Mockito, quel est le rôle d'un appel comme verify(gateway, never()).charge(anyString(), anyInt()) ?
Was this page helpful?