Día 11 de 30 Fase 2: De Vanilla a la Necesidad de Herramientas

Mockito y Dobles de Prueba: Tests Unitarios Aislados

Stubs, mocks, fakes y spies, @Mock e @InjectMocks, verify y ArgumentCaptor, y qué no conviene mockear

Para dominar. Al terminar deberías poder hacerlo sin ayuda.

  • 3.5h de estudio
  • 5 conceptos
  • 3 preguntas
  • 2 katas
Java 17 LTSJava 21 LTSMockitoJUnit 5Dobles de pruebaTDD
Al terminar este día sabrás 4
  • Elegir entre stub, mock, fake y spy según lo que quieres comprobar.
  • Escribir tests unitarios con MockitoExtension, @Mock e @InjectMocks.
  • Verificar efectos con verify y ArgumentCaptor sin acoplar el test a la implementación.
  • Decidir qué no mockear y cuándo pasar a un test de integración.

Tipos de Dobles: Stub, Mock, Fake y Spy

Un stub devuelve respuestas preparadas. Un mock además registra cómo lo llamaron para poder verificarlo. Un fake es una implementación real pero simplificada (un repositorio en memoria). Un spy envuelve un objeto real y permite sobrescribir solo algunos métodos.

En una línea Un TareaRepositoryEnMemoria implements TareaRepository con un HashMap es un fake; mock(TareaRepository.class) es un mock.

El mismo colaborador como fake y como mock java
public interface TareaRepository {
    Optional<Tarea> findById(long id);
    Tarea save(Tarea tarea);
}

// Fake: implementación real pero en memoria, reutilizable en todos los tests
public class TareaRepositoryEnMemoria implements TareaRepository {
    private final Map<Long, Tarea> datos = new HashMap<>();

    @Override public Optional<Tarea> findById(long id) { return Optional.ofNullable(datos.get(id)); }
    @Override public Tarea save(Tarea tarea) { datos.put(tarea.id(), tarea); return tarea; }
}

// Con el fake compruebas ESTADO
var repo = new TareaRepositoryEnMemoria();
repo.save(new Tarea(1L, "Escribir tests", false));
new TareaService(repo).completar(1L);
assertThat(repo.findById(1L)).get().extracting(Tarea::hecha).isEqualTo(true);

// Con el mock compruebas CONVERSACIÓN
TareaRepository mock = mock(TareaRepository.class);
when(mock.findById(1L)).thenReturn(Optional.of(new Tarea(1L, "Escribir tests", false)));
new TareaService(mock).completar(1L);
verify(mock).save(argThat(Tarea::hecha));
Cómo funciona por dentro

La terminología viene de Gerard Meszaros: «doble de prueba» es cualquier objeto que sustituye a uno real en un test. Un dummy solo rellena un parámetro. Un stub devuelve respuestas fijas. Un spy es un stub que además registra cómo lo llamaron. Un mock viene con expectativas sobre las llamadas que debe recibir. Un fake tiene una implementación que funciona, pero con un atajo: un repositorio en memoria, un servidor de correo falso.

Mockito difumina las diferencias: mock() crea un objeto que sirve como stub (when) y como mock (verify), y spy() envuelve un objeto real. Lo importante es saber qué estás comprobando: el estado resultante (con stubs o fakes) o la conversación entre objetos (con mocks).

Los fakes merecen más uso del que suelen tener. Un TareaRepositoryEnMemoria se escribe una vez, se reutiliza en todos los tests del servicio y permite comprobar el estado final (repo.findById(1)) en lugar de las llamadas internas, lo que hace los tests mucho más resistentes a los refactors.

Elegir el doble adecuado hace los tests más claros y menos frágiles. Muchos equipos abusan de los mocks donde un fake sería más sencillo.

Se conecta con Programar contra interfacesRepositorio en memoriaTests frágiles

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Tu Primer Test con Mockito (0 - 25 min)

    Qué estudiar. MockitoExtension, @Mock, @InjectMocks y when/thenReturn.

    Pregunta a responder. ¿Por qué la inyección por constructor facilita los tests?

    Resultado esperado. Test de TareaService.completar() con el repositorio mockeado, para el caso feliz y para una tarea inexistente.

  2. Bloque 2: Verificar Efectos (25 - 50 min)

    Qué estudiar. verify, never() y ArgumentCaptor.

    Pregunta a responder. ¿Cuándo tiene sentido verificar una interacción?

    Resultado esperado. Test que captura el email enviado al completar una tarea y comprueba destinatario y asunto.

  3. Bloque 3: Fakes y Límites (50 - 80 min)

    Qué estudiar. Repositorios en memoria y qué no conviene mockear.

    Pregunta a responder. ¿Qué partes de tu servicio deberías probar con Testcontainers en lugar de con mocks?

    Resultado esperado. El mismo test reescrito con un fake en memoria, comparando cuál se lee mejor.

  4. Cierre de Consolidación (80 - 90 min)

    Qué estudiar. Repaso de la rúbrica del tutor.

    Pregunta a responder. Explicar la diferencia entre stub, mock, fake y spy con un ejemplo de cada uno.

    Resultado esperado. Checklist personal de malos olores en tests con mocks.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Mock frente a fake: cuándo usar cada uno

Lo que debes entender

Un mock comprueba conversaciones: «¿me llamaste con esto?». Un fake se comporta como la pieza real, pero en memoria: guardas y después lees. Si lo que te importa es el estado final, un fake suele dar tests más legibles y más robustos.

Analogía para contarlo

Para ensayar una obra puedes tener un apuntador que comprueba que dijiste cada frase (mock) o un actor suplente que te contesta de verdad (fake).

Lo que suele explicarse mal

Llamar «mock» a cualquier doble de prueba. La diferencia importa porque cambia qué compruebas.

Señal de que aún no lo dominas

Si tus tests verifican cinco llamadas al repositorio en lugar de comprobar el resultado.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Qué diferencia hay entre @Mock y @MockitoBean? Ver respuesta

Respuesta razonada

@Mock pertenece a Mockito y funciona sin Spring: MockitoExtension crea el doble y tú construyes la clase con él. El test arranca en milisegundos.

@MockitoBean pertenece a Spring Test (Spring Framework 6.2, que usa Spring Boot 3.4 y posteriores, donde sustituye al antiguo @MockBean): crea el mock y lo registra como bean en el contexto de Spring, en lugar del bean real. Se usa en tests que necesitan Spring, como @WebMvcTest, donde los controladores reales reciben los servicios simulados. Cada combinación distinta de beans simulados obliga a crear un contexto nuevo, así que abusar de ellos ralentiza la suite.

Qué está evaluando la pregunta

@Mock es de Mockito puro: crea un doble en un test unitario sin Spring. @MockitoBean (Spring Boot 3.4+, antes @MockBean) sustituye un bean dentro del contexto de Spring, por ejemplo en un @WebMvcTest. El primero es mucho más rápido porque no arranca Spring.

Nivel 2 (Intermedio) Un test falla con UnnecessaryStubbingException. ¿Qué te está diciendo Mockito? Ver respuesta

Respuesta razonada

Que el test preparó una respuesta (when(...)) que el código nunca llegó a pedir. Puede ser un resto de una versión anterior del test, una copia de otro test o, lo más interesante, una señal de que el código no recorre el camino que creías: por ejemplo, sale antes por una validación.

El modo estricto existe para que los tests documenten solo lo que importa. Si un caso concreto necesita un stub opcional, se marca con lenient(), pero suele ser mejor separar ese test en dos.

Qué está evaluando la pregunta

Que preparaste un when(...) que el código nunca usó. O el stub sobra, o el código no sigue el camino que esperabas. El modo estricto de MockitoExtension lo señala para que los tests no acumulen preparación inútil que confunde a quien los lee.

Nivel 3 (Entrevista Senior) Tu equipo tiene 2.000 tests unitarios con mocks, todos en verde, y aun así fallan cosas en producción con la base de datos. ¿Qué está pasando? Ver respuesta

Respuesta razonada

Que los tests prueban las piezas por separado y dan por buenas todas las fronteras. Un repositorio mockeado nunca ejecuta su consulta, así que un JPQL mal escrito, una restricción NOT NULL, una transacción que no hace rollback o un mapeo JSON roto no fallan en ningún test.

La pirámide de testing pide pocos tests de integración, no ninguno: hace falta una capa con Testcontainers que ejecute el SQL contra la base de datos real, tests de MockMvc o REST Assured para el contrato HTTP y algún flujo completo. Además conviene revisar si los mocks simulan cosas que no son tuyas, porque es ahí donde la simulación y la realidad divergen.

Qué está evaluando la pregunta

Los mocks verifican la lógica suponiendo que las piezas externas se comportan como imaginas. Las consultas, las transacciones, las restricciones de la base de datos o la serialización nunca se ejecutan de verdad. Hacen falta tests de integración con Testcontainers en esas fronteras, y menos mocks de cosas que no son tuyas.

Reto del tutor

Revisando un pull request ves un test de PedidoService que mockea el repositorio, el mapper, el validador, el reloj y el propio PedidoService con spy, y verifica 9 llamadas con verify. Evalúa el test.

Lo que una respuesta suele tener bien

El test aísla el servicio de la base de datos.

Lo que suele quedarse en la superficie

Decir que «hay demasiados mocks» sin explicar el problema.

El concepto de fondo que suele faltar

No ver que verificar cada llamada acopla el test a la implementación: cualquier refactor lo rompe sin que cambie el comportamiento. Mockear el mapper y el validador (lógica pura) y hacer spy de la clase probada indica que el servicio tiene demasiadas responsabilidades.

Cómo lo respondería alguien senior en una entrevista

Mockear solo las fronteras (repositorio, clientes externos) e inyectar un Clock fijo; usar el mapper y el validador reales; comprobar el resultado o el estado final, y verificar solo las interacciones que son el objetivo del caso de uso, como que se publica un evento. Si sigue haciendo falta un spy, dividir el servicio.

Proyecto «Pedidos» · paso 11 de 30

Tests del servicio con dobles

Probar PedidoService sin base de datos y comprobar sus efectos.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Añade un Notificador que el servicio llama al confirmar un pedido.
  2. Prueba PedidoService con @Mock del repositorio y del notificador, y captura la notificación con ArgumentCaptor.
  3. Escribe un PedidoRepositoryEnMemoria (fake) y reescribe con él uno de los tests.

Está listo cuando

  • Los tests del servicio no arrancan base de datos ni Spring.
  • Ningún test verifica llamadas internas que no sean parte del comportamiento (como el orden de findById y save).

Katas: ejercicios cortos

Reglas de oro

  • Mockea las fronteras, no la lógica pura.
  • Comprueba el resultado; verifica interacciones solo cuando son el objetivo del test.
  • No mockees lo que no es tuyo: envuélvelo en una interfaz propia.
  • Un test, un comportamiento, con given/when/then visibles.

Java 17 frente a Java 21

CaracterísticaJava 17 LTSJava 21 LTS
Carga dinámica del agente de Mockito Sin avisos Avisa (JEP 451); configúralo como -javaagent
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown