Aunque ya hemos probado nuestro controlador de Pelicula con Postman, , solo hemos demostrado que ahora parece que funciona, pero, como no queremos tener que pasarnos un rato con Postman cada vez que nos pidan una modificación, debemos preparar el test de Junit
Planteamiento del test
Para poder probar el controlador, necesitaremos dar las mismas ordenes que se daran en produccion, pero no podemos permitirnos el que acceda a una base de datos
Por otra parte, el acceso a datos, lo probamos cuando escribimos el test de PeliculaService, y, ahora el controlador requerirá esos servicios, por lo que si podemos controlar el comportamiento de PeliculaService, podremos evitar la base de datos
Para lograrlo, solo tendremos que crear un PeliculaService falso, que devuelva el valor que nos interese en cada ocasión, y, eso que pare un trabajo ímprobo, se convierte en un juego de niños con Mockito.
Preparando el código
Nuestras primeras lineas establecerán la clase y unos cuantos campo que nos ayudaran a escribir el resto de código; algo como esto:
@WebMvcTest(controllers = PeliculaController.class)
class PeliculaControllerTestSpring {
@Autowired
private MockMvc mockMvc;
@Autowired
ObjectMapper mapper;
@MockBean
private PeliculaService cDao;
static final String STATUS = "$."+Constantes.STATUS;
static final String DATOS = "$."+Constantes.DATOS;
static final String MENSAJE = "$."+Constantes.MENSAJE;
static final String RUTA = "/api/pelicula";
static final String RUTAb = "/api/pelicula/";
static final int NUMERO_REGISTROS = 3;
Pelicula pelicula;
Pelicula peliculaOk;
Pelicula peliculaError;
Optional<Pelicula> peliculaOptional;
List<Pelicula> listaPeliculas;
Con la anotación @WebMvcTest, conseguimos que Spring se inicialice y quede disponible su contexto; concretamente PeliculaController, que es el test a realizar, tambien necesitamos que Spring nos inyecte el puntero de mockMvc, y el de ObjectMapper, y asi no tendremos que instanciar nosotros
Con la anotación @MockBean, conseguimos un objeto del servicio que vamos a manipular, y cuyas respuestas vamos a indicar antes de hacer las llamadas
La coleccion de constantes estaticas, nos simplifican el acceso a los JSON, y a algun literal mas
Por ultimo, creamos tambien algunos campos para tenerlos listos en cada test
El apartado común antes de cada test: (@BeforeEach)
Hemos dicho que cada metodo de un test ha de ser totalmente independiente de los demas, y pueden pasar en cualquier orden, eso significa, que todos los campos se han de inicializar ANTES de cada test, y eso lo podemos hacer con el método bajo anotación @BeforeEach
@BeforeEach
void setup() {
peliculaOk = new Pelicula(1L, "La pelicula correcta", 123);
this.peliculaError = new Pelicula(1L, null, 456);
this.pelicula = new Pelicula(2L, "Una pelicula",789);
this.peliculaOptional = Optional.of(pelicula);
this.listaPeliculas = Arrays.asList(pelicula, pelicula,pelicula);
}
Creamos los objetos que indicaremos a cDao (PeliculaService) que devuelva a nuestra conveniencia
Escribiendo test
Ahora ya podemos empezar a escribir los test, el primero, la lectura de un registro
@Test
void testLeerUno() throws Exception {
when(cDao.leerUno(1L)).thenReturn(peliculaOptional);
when(cDao.existe(1L)).thenReturn(true);
mockMvc.perform(get(RUTAb + 1)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath(STATUS, is(1)))
.andExpect(jsonPath(DATOS + ".id_pelicula", is(2)))
.andExpect(jsonPath(DATOS + ".pe_titulo", is("Una pelicula")))
.andExpect(jsonPath(DATOS + ".pe_identificador", is(789)))
;
}
Primero, condiciono la devolución de dos métodos de cDao, y, tengo que hacerlo así, ya que se llamara a @CheckPeliculaValidation, y utilizará existe que devolverá true, antes de hacer nada mas, y luego, llamará al servicio para hacer la lectura, en la que devolverá el objeto preparado peliculaOptional.
Para hacer la llamada, utilizaremos http para simular al máximo el comportamiento, de forma que utilizamos el método «GET» para efectuar la llamada /api/pelicula/1 y cuando llegue la respuesta comprobaremos los datos
- El status http que ha de ser 200
- Nuestro STATUS que ha de ser 1, correcto
- El Json contenido en DATOS y que campo a campo, ha de corresponder a los valores de película
Para la prueba de error, solo deberemos indicar a cDao.existe que retorne false, y, automáticamente, nuestro @CheckPeliculaValidation nos echará fuera
@Test
void testLeerUno_error() throws Exception {
Long id = 1L;
when(cDao.existe(id)).thenReturn(false);
mockMvc.perform(get(RUTAb + id)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().is4xxClientError())
.andExpect(jsonPath(STATUS, is(900)))
.andExpect(jsonPath(MENSAJE+"[\"leerUno.id\"]", containsString(Constantes.MSJ_ERROR_PELICULA_N)));
}
Atencion, estamos pasando también por nuestros ControllerAdvice, y eso lo deberemos considerar si queremos evaluar el mensaje devuelto, nuestro status, e, incluso, el status http, …….
Test de inserción
Para probar el metodo de insercion, solo nos debemos preocupar de lo que hace el controlador, por lo que decimos a cDao lo que tiene que hacer cuando se invoque al insert con cualquier valor de Película
@Test
void testAlta() throws Exception {
String peliculaJson = mapper.writeValueAsString(peliculaOk);
when(cDao.insert(any(Pelicula.class))).thenReturn(pelicula);
mockMvc.perform(post(RUTA)
.contentType(MediaType.APPLICATION_JSON)
.content(peliculaJson))
.andExpect(status().isCreated())
.andExpect(jsonPath(STATUS, is(1)))
.andExpect(jsonPath(DATOS + ".pe_titulo", is(pelicula.getPe_titulo())))
.andExpect(jsonPath(DATOS + ".pe_identificador", is(pelicula.getPe_identificador())));
}
En este caso, debemos enviar un json de película, lo que hacemos por medio del content, utilizando el método post, y al retornar, comprobamos el status http, nuestro STATUS, y el contenido de los campos del objeto pelicula, que han de coincidir con los valores que hemos pedido nos devuelva mockito en el insert
Para provocar el caso de error, utilizaremos un objeto película que no tiene título, y, como en el Entity exigimos titulo no nulo, realizara la función de error.
@Test
void testAltaErrorTitulo() throws Exception {
String peliculaJson = mapper.writeValueAsString(peliculaError);
this.mockMvc.perform(post(RUTA)
.accept(MediaType.TEXT_HTML)
.content(peliculaJson))
.andExpect(status().is4xxClientError());
}
Aunque, esta vez, no necesitamos condicionar la respuesta de cDao, ya que el error se detectara cuando se reciba el objeto, y directamente se generará la excepción e ira a nuestro @ControllerAdvice
Test para modificaciones
Una vez hecho los test de inserción, los de modificación son muy semejantes, utilizando el método PUT
@Test
void testModificacion() throws Exception {
String peliculaJson = mapper.writeValueAsString(peliculaOk);
when(cDao.update(any(Pelicula.class))).thenReturn(peliculaOk);
mockMvc.perform(put(RUTA)
.contentType(MediaType.APPLICATION_JSON)
.content(peliculaJson))
.andExpect(status().isOk())
.andExpect(jsonPath(STATUS, is(1)))
.andExpect(jsonPath(DATOS + ".pe_titulo", is(peliculaOk.getPe_titulo())))
.andExpect(jsonPath(DATOS + ".pe_identificador", is(peliculaOk.getPe_identificador())));
}
@Test
void testModificacionErrorTitulo() throws Exception {
String peliculaJson = mapper.writeValueAsString(peliculaError);
this.mockMvc.perform(put(RUTA)
.accept(MediaType.TEXT_HTML)
.content(peliculaJson))
.andExpect(status().is4xxClientError());
}
La forma de comprobar un put con datos erroneos, es, tambien, idéntica, utilizando el método PUT y enviando un objeto película al que le falta el titulo.
Los test para delete
Semejante a los métodos anteriores, pero utilizando delete, y asegurandonos que cubrimos cualquier acceso a cDao, proporcionando las respuestas que nos interese
@Test
void testEliminar() throws Exception {
Long id = 1L;
when(cDao.existe(id)).thenReturn(true);
when(cDao.borrarPorId(any(Long.class))).thenReturn(true);
mockMvc.perform(delete(RUTAb + id)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath(STATUS, is(1)))
.andExpect(jsonPath(MENSAJE, containsString (Constantes.MSJ_ELIMINACION_OK)));
}
@Test
void testEliminarError() throws Exception {
Long id = 1L;
when(cDao.existe(1L)).thenReturn(false);
mockMvc.perform(delete(RUTAb + id)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().is4xxClientError())
.andExpect(jsonPath(STATUS, is(900)))
.andExpect(jsonPath(MENSAJE+"[\"borrar.id\"]", containsString (Constantes.MSJ_ERROR_PELICULA_N)));
}
Las explicaciones de los casos anteriores, se repiten aquí. Debemos cubrir el cDao.existe porque el controlador realiza ese acceso en la validacion de @CheckPeliculaValidation, luego, también debemos interferir, en el caso correcto, la orden de borrarPorId, para que no se haga, pero, devuelva true, y luego, solo debemos verificar que recibimos los datos correctos
Conclusión
Todo este desarrollo lo tendreis explicado con mas detalle en youTube, y, aunque es conveniente que intentéis escribirlo TODO vosotros, si queréis renunciar a ello, lo teneis tambien en GitHub
Este desarrollo esta hecho para disponer de un fuente para explicar otros temas, tal y como se indica en Visión de conjunto con Spring