Ahora que he comprendido mejor el asunto tras haber estado pegándome con él largo y tendido, voy a explicar algunas de las dudas que a mí mismo me surgían cuando abordé el asunto por si a otro pajeet que esté aprendiendo le resultaran útiles:
En Spring Boot, al igual que en otros frameworks habrá soluciones análogas, existe la inyección de dependencia. Esta se representa con @Autowired y viene a ser el equivalente a definir un atributo en nuestro componente A que hubiera de ser igualado al argumento recibido por parámetro en un constructor de ese componente B que quisiéramos usar en nuestro componente A. Es decir:
import (...)
...
public class ComponenteA {
private ComponenteB componenteB;
public ComponenteA(ComponenteB componenteB) {
this.componenteB = componenteB;
}
...
}
que sería lo mismo que:
import (...)
...
public class ComponenteA {
@Autowired
ComponenteB componenteB;
...
}
En ambos casos podríamos usar en nuestro componente principal dicho componente inyectado. Sin embargo, haciéndolo a través de @Autowired no sólo estamos inyectando una instancia de dicho componente, sino que el propio framework comprueba que no exista ya otra instancia en el contexto y si fuera así, importa esa misma instancia, es decir, sería una referencia a dicha instancia con el consiguiente ahorro de memoria, algo así como una minifactoría de Singleton.
Definido esto, ahora vamos a los test, que era el quid de la cuestión del post.
Al hacer un test unitario, que no un test de integración, lo que queremos comprobar es el componente aislado del resto del contexto de la aplicación, por sí sólo. Para conseguir esto, hay anotaciones que aligeran la carga del contexto que se va a cargar al ejecutar dicho test. Porque, ¿para qué cargar todo el contexto de la aplicación cuando sólo queremos comprobar una parte de la misma?
Esto se consigue con
@ExtendWith(MockitoExtension.class) que básicamente lo que hace es cargar sólo la extensión Mockito en ese contexto, que es con la que haremos nuestros mocks de las dependencias de lo que estemos testeando y evitar que el contexto busque hacia arriba en el árbol hasta @SpringBootApplication para autoconfigurarse y cargar todo.
Una vez delimitado el contexto en el que se va a hacer el test, tenemos que aislar lo que queremos testear del resto, básicamente porque ese "resto" ya no existe al haber limitado el contexto que se va a cargar.
Para esto usamos las anotaciones @Mock que vienen a ser contenedores sin ningún tipo de funcionalidad más allá de la que nosotros le implementemos. Conservan el tipo, pero son "tontos" digamos. Aquí es donde entra en juego Mockito, que básicamente es una librería que sirve para establecer las directrices del comportamiento de estos elementos cuando haga falta.
Y el quid de la cuestión, y la razón de la introducción inicial, es el @InjectMocks.
Esta anotación lo que hace es sustituir la necesidad de la existencia del constructor en el ComponenteA para inyectar el componenteB, y me explico:
Igual que tenemos @Autowired para inyectar dependencias en el contexto de producción y ahorrarnos constructores y bueno, las ventajas que tiene per se, también tenemos @InjectMocks en el caso de los test unitarios para lo mismo. Si usáramos @Mock simplemente, necesitaríamos un constructor en el componente de producción A que estamos testeando en el test para que este supiera cómo usar el componente B mockeado que le pasamos porque al hacer
...
@Mock
ComponenteMockeadoB componenteMockeadoB;
@Test
public void miTestRandom() {
ComponenteA componenteA = new ComponenteA(componenteMockeadoB);
...
}
si el constructor no existiera nos daría error. Y si hemos utilizado @Autowired obviamente no va a existir porque es precisamente una de las ventajas de usar @Autowired, que nos ahorramos definir el constructor entre otras cosas. ¿Entonces qué soluciones hay? Una sería dejar el @Autowired junto con el constructor exprésamente para el test, en cuyo caso el test estaría causando que modificáramos nuestro componente original siendo un anti patrón, y la solución elegante sería usar @InjectMocks, que lo que haría sería exactamente lo que hace @Autowired en el componente original de producción, pero con los @Mocks que hayamos definido en ese contexto de test unitario. De tal forma que:
...
@Mock
ComponenteMockeadoB componenteMockeadoB;
@InjectMocks
ComponenteA componenteA;
@Test
public void miTestRandom() {
...
}
haría exactamente lo mismo que en el ejemplo anterior, pero sin necesidad de cambiar el código original con constructores. Nótese que no he puesto componenteMockeadoA sino componenteA porque la instancia de A no está mockeada, está instanciada del componente original pero sabiendo consumir los Mocks que le han sido inyectados. También mencionar que aunque he omitido la clase que contiene todo en los ejemplos, se supone que sería la clase del propio componente principal mas el sufijo del test que estemos haciendo, es decir CompomenteAMiTest o whatever.
Y un ejemplo práctico sería este:
package com.restaurant.backend.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
import com.restaurant.backend.dto.CategoriaDTO;
import com.restaurant.backend.service.CategoriaService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class CategoriaRestControllerUnitTest {
@Mock
CategoriaService categoriaService;
@Mock
CategoriaDTO categoriaDTO;
@InjectMocks
CategoriaRestController categoriaRestController;
@Test
public void testCategoriaRestControllerGetAll() {
List<CategoriaDTO> list = new ArrayList<>();
for (int i = 0; i < 3; i++) {
categoriaDTO.setId(i);
categoriaDTO.setNombre("categoria" + i);
list.add(categoriaDTO);
}
Mockito.when(categoriaService.getAll()).thenReturn(list);
assertEquals(list, categoriaRestController.getAll());
}
}
categoriaRestController desarrollaría su comportamiento real de componente, llamando a categoriaService.getAll() y haciendo las comprobaciones que tuviéramos en nuestro controller, si las hubiere, sólo que en este contexto, el categoriaService.getAll() ha sido mockeado para no tener que depender de todas las capas anteriores para devolver la lista de DTO que en este caso el cómo lo haga nos da igual porque estamos comprobando el método getAll() y sólo ese método.
Y hasta aquí el chapazo espero que le sirva a algún pajeet que se vea en la misma tesitura alguna vez.