W3docs

Mocking en Java con Mockito

Simula dependencias en pruebas Java con Mockito: mock, when/thenReturn, verify y captores de argumentos.

Una prueba unitaria debe ejercitar una clase de forma aislada. Pero las clases reales dependen de colaboradores — una base de datos, una pasarela de pago, un enviador de correos — que son lentos, poco fiables o tienen efectos secundarios que no deseas en una prueba. Mockito es la biblioteca Java más utilizada para reemplazar esos colaboradores con mocks: objetos sustitutos que programas para devolver respuestas predefinidas y luego interrogas sobre cómo fueron llamados. Este capítulo muestra la API de Mockito que escribirás cada día, y demuestra la idea subyacente con un programa en JDK puro que puedes ejecutar aquí mismo.

Este capítulo asume que ya conoces los conceptos básicos de pruebas cubiertos en Introducción a JUnit 5 y Aserciones en JUnit. Mockito complementa JUnit — JUnit ejecuta la prueba y verifica los valores, mientras que Mockito suministra los colaboradores falsos.

Por qué usar mocks

La clase bajo prueba (el sistema bajo prueba, o SUT) normalmente recibe sus colaboradores a través de su constructor — eso es lo que ofrece la inyección de dependencias. En una prueba le pasas un colaborador falso en lugar del real. Un buen falso hace dos cosas:

  • Stubbing — devuelve el valor que el escenario de prueba necesita (charge(...) devuelve true, o lanza una excepción), para que puedas llevar al SUT por un camino específico sin una llamada de red real.
  • Verificación — registra cada llamada que recibió, para que luego la prueba pueda afirmar que el SUT lo llamó de la manera correcta, el número correcto de veces y con los argumentos correctos.

Mockito genera tal falso para cualquier interfaz o clase no final en tiempo de ejecución, por lo que nunca tienes que escribirlo a mano. Pero conocer lo que genera hace que la API sea obvia.

Crear mocks y definir retornos con stub

Mockito.mock(Type.class) produce un mock. Por defecto, cada método devuelve un valor vacío "amigable" — null para objetos, false para booleanos, 0 para números. Luego sobreescribes los métodos que te interesan con 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"));

Para métodos void el orden se invierte: doThrow(...).when(mock).method(). Los stubs también pueden flexibilizarse con comparadores de argumentos como anyString() y anyInt() para que se activen ante cualquier llamada, no solo un conjunto exacto de argumentos.

Verificar interacciones

Después de que el SUT se ejecuta, verify(...) afirma cómo se usó el mock. Así es como pruebas los efectos secundarios — un correo que debería haberse enviado, una fila que debería haberse guardado — sin inspeccionar el sistema real.

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

Los modos de verificación más comunes:

ModoSignificado
times(n)Llamado exactamente n veces
never()Equivale a times(0)
atLeastOnce() / atLeast(n)Llamado al menos una vez / n veces
atMost(n)Llamado como máximo n veces
only()Este fue el único método llamado en el mock

Capturar argumentos

Cuando necesitas inspeccionar qué se pasó — no solo que ocurrió una llamada — usa un ArgumentCaptor. Captura el argumento real para que puedas hacer afirmaciones sobre sus campos, lo cual es invaluable cuando el SUT construye un objeto antes de pasarlo.

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 y espías

En clases de prueba reales rara vez llamas a mock() manualmente. Las anotaciones conectan todo: @Mock declara un campo mock, @InjectMocks construye el SUT e inyecta los mocks en su constructor, y @ExtendWith(MockitoExtension.class) (JUnit 5) activa el procesamiento.

@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)) es el punto intermedio: envuelve un objeto real y ejecuta métodos reales a menos que los definas con stub — útil para el mocking parcial de código heredado.

Advertencia
Mockito solo puede mockear lo que es sobreescribible. Por defecto no puede mockear clases final, métodos final, métodos static ni métodos private. Si debes mockear una clase final, habilita el MockMaker mockito-inline; de lo contrario, refactoriza hacia una interfaz.

Cuándo no usar mocks

Los mocks son poderosos, pero el exceso de mocking produce pruebas que pasan mientras el código real está roto. Usa un mock solo cuando el colaborador real es lento, no determinista, tiene efectos secundarios o aún no está construido. No mockees objetos de valor, la propia clase bajo prueba ni tipos que no posees (envuelve una API de terceros en tu propia interfaz y mockea eso). Cuando el colaborador es barato y puro — una calculadora simple, una lista en memoria — usa el real y haz afirmaciones directamente sobre su resultado.

Un ejemplo práctico: un mock construido a mano

Mockito en sí no está en el classpath de esta página, por lo que el programa ejecutable a continuación construye el mock a mano — una pequeña clase que implementa la interfaz de dependencia, contiene un valor de retorno definido con stub y registra cada llamada. Esta es exactamente la maquinaria que Mockito genera para ti en tiempo de ejecución, por lo que leerla te dice exactamente qué hacen when/thenReturn y verify internamente.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • El stubbedResult = true del MockGateway es la forma escrita a mano de when(gateway.charge(...)).thenReturn(true); como el stub devolvió true, el SUT imprimió result : PAID sin que ocurriera ningún pago real.
  • invocationCount == 1 imprimiendo true es exactamente lo que verifica verify(gateway).charge(...) — el mock contó que fue llamado una vez, que es la manera en que Mockito convierte "¿ocurrió esta interacción?" en una aserción de éxito o fallo.
  • La lista calls capturó charge(acct-7, 1999), la idea de captura de argumentos detrás de ArgumentCaptor: un mock recuerda no solo que fue llamado sino con qué, para que la prueba pueda hacer afirmaciones sobre los argumentos reales.
  • Recrear el mock con stubbedResult = false llevó al SUT por su otra rama e imprimió declined result : DECLINED, mostrando cómo un falso te permite guionizar cada escenario que el colaborador real podría producir.
  • La cláusula de guarda devolvió INVALID antes de llegar a la pasarela, por lo que invocationCount == 0 imprimió true — la prueba ejecutable de verify(gateway, never()).charge(...), que afirma que una dependencia fue deliberadamente no tocada.

Práctica

Práctica
En una prueba unitaria basada en Mockito, ¿cuál es el propósito de una llamada como verify(gateway, never()).charge(anyString(), anyInt())?
En una prueba unitaria basada en Mockito, ¿cuál es el propósito de una llamada como verify(gateway, never()).charge(anyString(), anyInt())?
Was this page helpful?