# Día 11: Mockito y Dobles de Prueba: Tests Unitarios Aislados

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

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 11 de 30
- Fase 2: De Vanilla a la Necesidad de Herramientas
- Dedicación: 3.5h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/mockito-dobles-de-prueba/

## Al terminar este día sabrás

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


## Conceptos

### 1. 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.

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

*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));
```

- **En una línea:** Un `TareaRepositoryEnMemoria implements TareaRepository` con un `HashMap` es un fake; `mock(TareaRepository.class)` es un mock.
- **Por qué importa:** 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.
- **Error típico:** Usar `spy` sobre la propia clase que pruebas para cambiar parte de su comportamiento: es señal de que la clase hace demasiadas cosas.
- **Java 17 frente a 21:** Concepto independiente de la versión de Java.
- **Se conecta con:** Programar contra interfaces, Repositorio en memoria, Tests frágiles

### 2. @Mock, @InjectMocks y when/thenReturn

Con `@ExtendWith(MockitoExtension.class)`, `@Mock` crea los dobles e `@InjectMocks` crea la clase bajo prueba inyectándoselos por constructor. `when(repo.findById(1L)).thenReturn(Optional.of(tarea))` define respuestas y `thenThrow` simula errores.

**Cómo funciona por dentro.**

`@ExtendWith(MockitoExtension.class)` activa Mockito en JUnit 5: crea los campos anotados con `@Mock` antes de cada test y construye el campo `@InjectMocks` pasando los mocks al constructor más grande que encuentre. Si tu clase recibe sus dependencias por constructor, como recomienda Spring, la inyección es directa; también puedes prescindir de `@InjectMocks` y hacer `new Servicio(repo, reloj)` en el `@BeforeEach`.

Un mock sin configurar devuelve valores vacíos: `null` para objetos, `0`, `false`, `Optional.empty()` y colecciones vacías. Con `when(...).thenReturn(...)` defines respuestas; con `thenThrow`, errores; con `thenAnswer`, respuestas calculadas a partir de los argumentos. Los matchers (`any()`, `eq()`, `argThat(...)`) amplían qué llamadas encajan, con una regla: si usas uno en una llamada, todos los argumentos de esa llamada deben ser matchers.

`MockitoExtension` usa por defecto el modo estricto: falla si un stub no se usa (`UnnecessaryStubbingException`) o si se llama al mock con argumentos distintos de los preparados. Al principio molesta, pero mantiene los tests honestos.

*Test unitario de un servicio con Mockito*

```java
@ExtendWith(MockitoExtension.class)
class TareaServiceTest {

    @Mock TareaRepository repo;
    @Mock Notificador notificador;
    @InjectMocks TareaService servicio;           // new TareaService(repo, notificador)

    @Test
    void completar_marca_la_tarea_y_avisa_al_responsable() {
        var tarea = new Tarea(1L, "Revisar PR", false, "ana@acme.com");
        when(repo.findById(1L)).thenReturn(Optional.of(tarea));
        when(repo.save(any(Tarea.class))).thenAnswer(inv -> inv.getArgument(0));

        Tarea resultado = servicio.completar(1L);

        assertThat(resultado.hecha()).isTrue();
        verify(notificador).enviar(eq("ana@acme.com"), contains("Revisar PR"));
    }

    @Test
    void completar_una_tarea_inexistente_falla() {
        when(repo.findById(99L)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> servicio.completar(99L))
            .isInstanceOf(TareaNoEncontradaException.class);
        verifyNoInteractions(notificador);
    }
}
```

- **En una línea:** `when(pagos.cobrar(any())).thenThrow(new PagoRechazadoException());`
- **Por qué importa:** Permite probar la lógica de un servicio sin base de datos ni red: tests de milisegundos que solo fallan cuando cambia la lógica.
- **Error típico:** Preparar stubs que nunca se usan: con `MockitoExtension` (modo estricto) el test falla con `UnnecessaryStubbingException`, y es una buena señal de que el test está mal enfocado.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Inyección por constructor, JUnit 5, BDDMockito

### 3. Verificar Interacciones: verify y ArgumentCaptor

`verify(mock).metodo(args)` comprueba que se llamó a un método; `times(2)`, `never()` y `verifyNoMoreInteractions` afinan la comprobación. `ArgumentCaptor` captura el argumento real para inspeccionarlo con aserciones.

**Cómo funciona por dentro.**

`verify(mock).metodo(args)` falla si ese método no se llamó exactamente una vez con esos argumentos. Con `times(n)`, `atLeastOnce()` o `never()` cambias la cuenta; `verifyNoInteractions(mock)` comprueba que no se tocó y `inOrder(a, b)` que las llamadas ocurrieron en un orden concreto.

Cuando el argumento es un objeto construido dentro del método (un email, un evento), comprobarlo con un matcher se vuelve ilegible. `ArgumentCaptor` lo captura durante el `verify` y te lo devuelve para inspeccionarlo con AssertJ como cualquier otro objeto. Con `@Captor` puedes declararlo como campo.

La pregunta clave es si la interacción forma parte del comportamiento que quieres garantizar. «Al confirmar un pedido se publica un evento» es una regla de negocio: verifícala. «El servicio llama a `findById` y luego a `save`» es un detalle de implementación: comprueba el resultado y deja libre el cómo.

*Capturar el evento publicado*

```java
@ExtendWith(MockitoExtension.class)
class PedidoServiceTest {

    @Mock PedidoRepository repo;
    @Mock EventPublisher eventos;
    @Captor ArgumentCaptor<PedidoConfirmado> evento;

    Clock reloj = Clock.fixed(Instant.parse("2026-10-28T09:00:00Z"), ZoneOffset.UTC);

    @Test
    void confirmar_publica_un_evento_con_la_fecha_del_reloj() {
        when(repo.findById(7L)).thenReturn(Optional.of(Pedido.nuevo(7L)));
        var servicio = new PedidoService(repo, eventos, reloj);

        servicio.confirmar(7L);

        verify(eventos).publicar(evento.capture());
        assertThat(evento.getValue())
            .extracting(PedidoConfirmado::pedidoId, PedidoConfirmado::confirmadoEn)
            .containsExactly(7L, Instant.parse("2026-10-28T09:00:00Z"));
        verifyNoMoreInteractions(eventos);              // y no se publicó nada más
    }
}
```

- **En una línea:** `var captor = ArgumentCaptor.forClass(Email.class); verify(mailer).enviar(captor.capture()); assertThat(captor.getValue().destino()).isEqualTo("ana@acme.com");`
- **Por qué importa:** Hay efectos que no se ven en el valor de retorno, como un email enviado o un evento publicado. Para esos casos se verifica la interacción.
- **Error típico:** Verificar cada llamada interna del servicio: el test se rompe con cualquier refactor aunque el comportamiento no cambie.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** Efectos secundarios, Eventos de dominio, AssertJ

### 4. Qué No Conviene Mockear

No se mockean los value objects ni los records (se crean y ya está), ni los tipos que no son tuyos como `HttpClient` o `EntityManager`, ni la propia clase que pruebas. La base de datos real se prueba con tests de integración y Testcontainers.

**Cómo funciona por dentro.**

Mockear tiene un coste: el doble se comporta como tú crees que se comporta la pieza real. Si esa creencia es falsa, el test pasa y el código falla. El riesgo es pequeño con interfaces tuyas, cuyo contrato defines tú, y grande con APIs ajenas complejas: `EntityManager`, `JdbcTemplate`, `HttpClient`, `KafkaTemplate`. Mockearlas obliga además a reproducir en el test su forma de uso, y el test se rompe en cuanto cambias de librería.

La regla «no mockees lo que no es tuyo» lleva a un diseño mejor: envuelve la dependencia externa en un adaptador propio con una interfaz pequeña, expresada en términos de tu dominio (`ProveedorDeTipos.cotizacion(moneda)`). En los tests unitarios mockeas esa interfaz, y el adaptador real se prueba aparte con un test de integración (Testcontainers para la base de datos, WireMock para HTTP). Es la misma idea de puertos y adaptadores de la arquitectura hexagonal.

Tampoco se mockean los objetos de valor (records, DTO, entidades sin dependencias): crearlos es más fácil y más claro que configurar un mock. Y nunca la clase que estás probando.

*Adaptador propio en lugar de mockear la librería*

```java
// Puerto: interfaz pequeña en términos del dominio
public interface ProveedorDeTipos {
    BigDecimal cotizacion(String moneda);
}

// Adaptador real: se prueba con un test de integración (por ejemplo, con WireMock)
class ProveedorDeTiposHttp implements ProveedorDeTipos {
    private final RestClient rest;

    ProveedorDeTiposHttp(RestClient rest) { this.rest = rest; }

    @Override
    public BigDecimal cotizacion(String moneda) {
        return rest.get().uri("/tipos/{m}", moneda).retrieve().body(Cotizacion.class).valor();
    }
}

// Test unitario del servicio: se mockea el puerto, no RestClient
@Test
void convierte_con_la_cotizacion_del_proveedor() {
    ProveedorDeTipos tipos = mock(ProveedorDeTipos.class);
    when(tipos.cotizacion("USD")).thenReturn(new BigDecimal("1.08"));

    var precio = new Conversor(tipos).aDolares(new BigDecimal("100"));

    assertThat(precio).isEqualByComparingTo("108.00");
}
```

- **En una línea:** Envuelve el cliente externo en tu propia interfaz (`ProveedorDeTipos`) y mockea esa interfaz, no `HttpClient`.
- **Por qué importa:** Un test lleno de mocks de librerías comprueba tu suposición sobre cómo funcionan, no si funcionan. Pasa en verde y falla en producción.
- **Error típico:** Mockear `JdbcTemplate` o el `EntityManager` para probar una consulta: el SQL nunca se ejecuta y sus errores pasan desapercibidos.
- **Java 17 frente a 21:** Concepto independiente de la versión de Java.
- **Se conecta con:** Testcontainers, Pirámide de testing, Arquitectura hexagonal

### 5. Estilo Given/When/Then y BDDMockito

Un test bien escrito tiene tres partes: preparar el escenario (given), ejecutar la acción (when) y comprobar el resultado (then). `BDDMockito` ofrece `given(...).willReturn(...)` y `then(mock).should()` para que el código se lea igual que esas tres partes.

**Cómo funciona por dentro.**

Given/when/then estructura cada test en tres bloques visibles: el contexto de partida, la única acción que se prueba y las consecuencias esperadas. Viene del desarrollo guiado por comportamiento (BDD), que propone escribir los tests como ejemplos concretos de reglas de negocio, con nombres que se leen como frases: `completar_una_tarea_bloqueada_falla`.

`BDDMockito` es un alias de la API de Mockito con ese vocabulario: `given(repo.findById(1L)).willReturn(...)` en lugar de `when(...).thenReturn(...)`, y `then(eventos).should().publicar(...)` en lugar de `verify(...)`. No cambia lo que hace el test, solo cómo se lee, y evita la confusión de escribir `when` en la sección given.

La misma estructura escala hasta Cucumber, donde los escenarios se escriben en lenguaje natural (`Dado`, `Cuando`, `Entonces`) y se conectan con código Java. La disciplina es la misma en los dos niveles: una sola acción en el bloque when y aserciones que describen el resultado, no la implementación.

*El mismo test con vocabulario BDD*

```java
import static org.mockito.BDDMockito.*;

@Test
void completar_una_tarea_bloqueada_falla_y_no_notifica() {
    // given
    var bloqueada = new Tarea(5L, "Desplegar", false, "ana@acme.com").bloquear("falta aprobación");
    given(repo.findById(5L)).willReturn(Optional.of(bloqueada));

    // when
    var error = catchThrowable(() -> servicio.completar(5L));

    // then
    assertThat(error)
        .isInstanceOf(TareaBloqueadaException.class)
        .hasMessageContaining("falta aprobación");
    then(notificador).shouldHaveNoInteractions();
    then(repo).should(never()).save(any());
}
```

- **En una línea:** `given(repo.findById(1L)).willReturn(Optional.empty()); assertThatThrownBy(() -> servicio.completar(1L)).isInstanceOf(TareaNoEncontradaException.class);`
- **Por qué importa:** Los tests son documentación ejecutable: si se leen como una especificación, cualquiera entiende qué regla de negocio protegen. Es la misma idea que usa Cucumber con lenguaje natural.
- **Error típico:** Tests con varios when/then mezclados que prueban cinco cosas a la vez: cuando fallan, no sabes cuál.
- **Java 17 frente a 21:** Igual en Java 17 y 21.
- **Se conecta con:** BDD, Cucumber, Nombres de tests


## Sesión de 90 minutos

### 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?
- **Entregable:** Test de `TareaService.completar()` con el repositorio mockeado, para el caso feliz y para una tarea inexistente.

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

- **Qué estudiar:** `verify`, `never()` y `ArgumentCaptor`.
- **Pregunta a responder:** ¿Cuándo tiene sentido verificar una interacción?
- **Entregable:** Test que captura el email enviado al completar una tarea y comprueba destinatario y asunto.

### 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?
- **Entregable:** El mismo test reescrito con un fake en memoria, comparando cuál se lee mejor.

### 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.
- **Entregable:** Checklist personal de malos olores en tests con mocks.


## Explícalo desde cero

### Mock frente a fake: cuándo usar cada uno

- **Qué 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.
- **Qué suele explicarse mal:** Llamar «mock» a cualquier doble de prueba. La diferencia importa porque cambia qué compruebas.
- **Ejemplo sencillo:** 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).
- **Señal de alerta:** Si tus tests verifican cinco llamadas al repositorio en lugar de comprobar el resultado.


## Preguntas

### Nivel 1 (Básico): ¿Qué diferencia hay entre `@Mock` y `@MockitoBean`?

`@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.

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

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

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.

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

### 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?

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.

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


## Reto del tutor

**Reto:** 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 está bien:** El test aísla el servicio de la base de datos.
- **Respuesta superficial:** Decir que «hay demasiados mocks» sin explicar el problema.
- **Lo que falta de fondo:** 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.
- **Respuesta senior:** 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.


## Katas

### Kata 11.1: Servicio de pedidos aislado

Escribe `PedidoService.confirmar(id)` que busca el pedido, lo marca como confirmado y publica un `PedidoConfirmado` a través de un `EventPublisher`. Pruébalo con `@Mock` del repositorio y del publisher, captura el evento con `ArgumentCaptor` y comprueba con AssertJ su id y su fecha usando un `Clock` fijo.

### Kata 11.2: Del mock al fake

Reescribe el test anterior sustituyendo el repositorio mockeado por un `PedidoRepositoryEnMemoria` (fake). Compara la legibilidad de las dos versiones y anota cuál prefieres y por qué.


## 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ística | Java 17 | Java 21 |
| --- | --- | --- |
| Carga dinámica del agente de Mockito | Sin avisos | Avisa (JEP 451); configúralo como -javaagent |


## Proyecto guiado «Pedidos»: Tests del servicio con dobles

Probar `PedidoService` sin base de datos y comprobar sus efectos.

**Tareas:**

- Añade un `Notificador` que el servicio llama al confirmar un pedido.
- Prueba `PedidoService` con `@Mock` del repositorio y del notificador, y captura la notificación con `ArgumentCaptor`.
- 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`).


## Fuentes oficiales

- [Mockito](https://site.mockito.org/)
- [Javadoc de Mockito](https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html)
- [Martin Fowler: Mocks Aren't Stubs](https://martinfowler.com/articles/mocksArentStubs.html)

