Mockito y JUnit5

B

Buenas gente de bien.

Estoy intentando incluir los test unitarios en un proyecto casi acabado para practicar -obviemos que debería haberlos hecho durante, pero estás que acababa si no- y no sé cómo definir la implementación de una interfaz que lo único que hace es heredar de una librería. Es decir, sé interpretar el error, pero no sé cómo abordarlo. Lo que quiero es testear que el repositorio me devuelva una lista. ¿Alguna pista?

adriminoria

Simplemente lanza una batería de pruebas con tus procedimientos comprobando posibles casos de error, principalmente en los casos extremos, y que genere mensajes de error cuándo se produzca alguna de las circunstancias que considerarías erróneas (resultados imposibles, incoherencias entre los resultados, etc.) y mensajes para cuando se realice el test satisfactoriamente.

Cada test heredaría de las partes del sistema que necesite para realizar las pruebas correspondientes, y crearía una implementación concreta del sistema planteada para la prueba que quieras realizar. Es decir, tu test realizaría un lanzamiento en concreto del sistema con unos parámetros para poder comprobar ciertas circunstancias concretas que tú mismo diseñarás para dicho test.

Estos test unitarios se pueden incluir en diversas baterías de pruebas que se lanzarían de golpe para que realicen todos los test de dicha batería y poder ver de un vistazo cuál es la parte de tu sistema que falla.

Luego deberías tener un procedimiento para lanzar todas las baterías juntas y de tal manera realizar una prueba completa del sistema.

Espero que te sea de ayuda cualquier cosa me puedes preguntar, estuve un par de años trabajando en pruebas unitarias en Ada así que algo de experiencia tengo en dichos test, aunque ya te digo que la naturaleza de las pruebas que realices dependen por completo del proyecto que tengas entre manos.

Kaledros

Cambia @InjectMocks por @Mock

JuAn4k4

Me da a mi que tu test está probando que Mockito funciona bien y no tu repositorio.

1
Ranthas

InjectMocks CREA una instancia de la clase, por tanto, no puede ser una interfaz.

Viendo que tus repositorios son solo interfaces, supongo que estarás heredando de JpaRepository o alguno de sus padres; si es así, tienes varias opciones:

  • Cambiar InjectMocks por Mock
  • Echarle un vistazo a DataJpaTest
B

Algo era ello, ty

B

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 :sweat_smile: espero que le sirva a algún pajeet que se vea en la misma tesitura alguna vez.

1 respuesta
JuAn4k4

#7 Puedes usar @Inject en vez de autowired y puedes usarlo en el ctor. Así puedes crear cosas a mano más claramente en tests.

B

Por lo que veo @Inject sería lo mismo que @Autowired pero resuelto por JakartaEE en vez de Spring Boot, similar a usar save() en vez de persist() con JPA o JPA bajo Hibernate respectivamente, pero al final hacen lo mismo, ¿a eso te referías? Lo de especificar a qué constructor quieres asociar el @Autowired cuando hay más de uno ya vi que se podía sí, incluso era necesario en versiones anteriores, pero tampoco le presté mucha atención a ese detalle. Le echaré un ojo a ver si me dejé algo en el tintero. Gracias.

Usuarios habituales