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(...)devuelvetrue, 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 happenedLos modos de verificación más comunes:
| Modo | Significado |
|---|---|
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.
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.
Lo que se puede extraer de la ejecución:
- El
stubbedResult = truedelMockGatewayes la forma escrita a mano dewhen(gateway.charge(...)).thenReturn(true); como el stub devolviótrue, el SUT imprimióresult : PAIDsin que ocurriera ningún pago real. invocationCount == 1imprimiendotruees exactamente lo que verificaverify(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
callscapturócharge(acct-7, 1999), la idea de captura de argumentos detrás deArgumentCaptor: 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 = falsellevó 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ó
INVALIDantes de llegar a la pasarela, por lo queinvocationCount == 0imprimiótrue— la prueba ejecutable deverify(gateway, never()).charge(...), que afirma que una dependencia fue deliberadamente no tocada.