Aunque reconozco que hoy en día parece absurdo tener que hablar de lo que representa Junit5, y el porqué se han de hacer test, la verdad es que todavía hay muchos programadores «profesionales» que se rebelan con la idea de hacer test.
Todos somos «muy buenos», y nuestros programas no fallan nunca, si, lo se, pero… que quereis que os diga, yo me voy a dormir más tranquilo si he podido comprobar que ese postulado es cierto!.
Tambien sé, que a pesar de eso, mañana estaré corriendo para arreglar un maldito error que no ví, pero… espero que solo sea uno, y, además, y más importante, mientras hago esa corrección podré volver a comprobar TODA mi aplicación, para verificar que esa modificación no genera otro error!
Realmente, pienso que el responsable de un proyecto no debería dar por acabado el proyecto hasta no tener un porcentaje muy alto de dicho proyecto cubierto con test. Esta filosofía, ya esta cubierta en los sistemas de despliegue automático, y deberíamos implementarlas hasta para hacer el mas pequeño trabajo, y, por eso, aunque no os convenza, vamos a utilizar los proximos capitulos para ver como podemos preparar test para nuestra aplicación de Los Cines….
Un test básico en Java
Nuestra primera propuesta, será hacer el test de cualquier clase, tiempo atrás, hicimos el test de un módulo estático como Rutinas, y ahora lo vamos a realizar de un controlador… aunque lo probaremos método a método, sin preocuparnos del sistema de redireccionamiento.
Vamos a probar únicamente el CineController. Esta clase, llama a la clase CineServicio para hacer cualquier cosa, y, nosotros no queremos evaluar AHORA CineServicio, con lo que utilizaremos Mockito para ocultarla.
Para poder escribirlo, necesitaremos las librerías de Junit5, aunque posiblemente ya estarán en pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Tambien crearemos una colección de paquetes reflejos, pero dentro de la zona «test»
Aunque, posiblemente, esta estructura tambien se habrá creado a la vez que proyecto
Ahora, vamos a crear la estructura de nuestra clase, con el boton derecho sobre CineController
He tenido que seleccionar Other, porque no me ha ofrecido JunitTestCase
Ahora, ya puedo seleccionarlo, comprobar la informacion que aparece
Aunque pulsaré «Next» para utilizar la siguiente pantalla
Y, en ella, seleccionar inicialmente los métodos que voy a probar; cuando pulsemos en Finish, se nos generará, en la zona de test, una clase preparada para que vayamos escribiendo nuestras pruebas, aunque funcionando desde el principio, dando error en todos los test.
Si se nos ocurre probar de hacer correr el test
Veremos, en la pantalla de Test que todos los métodos han fallado, y, en la parte inferior el motivo de error
Con eso, ya habeis visto como funciona el test, ahora solo falta hacerlo, y para ello, y por mi organización, le voy a cambiar el nombre a la clase a CineControllerTestJava, añadiendo la anotación
@ExtendWith(MockitoExtension.class)
class CineControllerTestJava {
Primero necesito falsear CineService, por lo que construyo uno falso
@Mock
private CineService cDao;
A continuación, genero una serie de atributos que voy a utilizar al hacer las pruebas, ya que tendré que enviar datos, y comprobar que lo que me devuelve es lo esperado; de forma que pienso en estos
Cine cine;
Cine cineExistente;
Cine cineErrorNombre;
Cine cineErrorCalle;
Cine cineErrorCapacidad;
String cineJson;
String cineExistenteJson;
String cineErrorNombreJson;
String cineErrorCalleJson;
String cineErrorCapacidadJson;
Optional<Cine> cineOptional;
CineProjectionNombre cineProjectionNombre;
Tambien necesito un ObjectMapper para hacer conversiones a JSON o entre Cine y CineDTO, y un atributo para guardar la direccion del controlador…que como veis, no utilizo el Autowired….
ObjectMapper objectMapper = new ObjectMapper();
CineController cineController;
CineDTO cineDTO;
Como en este test trabajaremos sin base de datos, necesito preparar las entradas, y eso lo hare en un método @BeroreEach que se ejecuta antes de cada test que se vaya a correr
@BeforeEach
void setup() throws JsonProcessingException {
this.cine = new Cine(1L, "Cine1", "Calle 1","Barrio 1", 300,null);
this.cineDTO = new CineDTO(1L, "Cine1", "Calle 1","Barrio 1", 300,null);
this.cineJson = this.objectMapper.writeValueAsString(cine);
this.cineExistente = new Cine(10L, "Cine10", "Calle 10","Barrio 10", 500,null);
this.cineExistenteJson = this.objectMapper.writeValueAsString(cineExistente);
this.cineErrorNombre = new Cine(0L, "", "Calle 1","Barrio 1", 300, null);
this.cineErrorCalle = new Cine(0L, "Cine1", "","Barrio 1", 300, null);
this.cineErrorCapacidad = new Cine(0L, "Cine1", "Calle 1","Barrio 1", 0, null);
this.cineErrorNombreJson = this.objectMapper.writeValueAsString(cineErrorNombre);
this.cineErrorCalleJson = this.objectMapper.writeValueAsString(cineErrorCalle);
this.cineErrorCapacidadJson = this.objectMapper.writeValueAsString(cineErrorCapacidad);
this.cineOptional = Optional.of(cine);
this.cineProjectionNombre = new CineProjectionNombre(1L, "Cine1", "Barrio 1");
cineController = new CineController(cDao);
}
Todo muy normal, creo instancias con new, y preparo los objetos según me interese. Solo quiero que observéis la carga del controlador (cineController = new CineController(cDao)), en donde el objeto dao que le paso, no es uno recibido de spring, sino uno creado con Mockito, esto es, falso, que luego podré manipular según me interese.
Otra cosa que tengo que preparar son funciones que me permitan comparar los objetos CineDTO que reciba, para ver si son iguales a los que espero, y, lo mismo que las listas, ya que eso es lo que voy a ir recibiendo cuando llame a un método.
No son demasiado complicadas; solo un poco molestas, pero…
/**
* Compara dos objetos CineDTO devolviendo true/false
* @return
*/
public boolean compararCines(CineDTO cine, CineDTO other) {
if (cine == null && other == null) {
return true;
} else if (cine == null || other == null) {
return false;
}
return cine.getCi_barrio().compareTo(other.getCi_barrio())==0
& cine.getCi_calle().compareTo(other.getCi_calle())==0
& cine.getCi_capacidad() == other.getCi_capacidad()
& cine.getCi_nombre().compareTo(other.getCi_nombre())==0
& cine.getId_cine()==other.getId_cine();
}
/**
* Compara dos listas de CineDTO, comprobando que tienen los mismos elementos,
* aunque no obliga a que esten en el mismo orden
*/
public boolean compararListas(List<CineDTO> lista1,List<CineDTO> lista2) {
List<CineDTO> lista = lista1.stream()
.filter(f-> !listaContain(f,lista2))
.collect(Collectors.toList());
return lista.size()==0 && lista1.size()==lista2.size();
}
public boolean listaContain(CineDTO obj, List<CineDTO> lista1) {
for (CineDTO element : lista1) {
if (!compararCines(element, obj)) {
System.out.println("element -" + element);
System.out.println("obj-" + obj);
return false;
}
}
return true;
}
Y con todo esto resuelto, vamos a realizar nuestro primer test, por ejemplo del método leerTodos. En este caso, nosotros no tenemos que pedir nada, y el controlador a de devolver una lista de CineDTO.
Como no vamos a acceder a la base de datos, deberemos instruir a nuestro Dao de lo que tiene que devolver, para ello
List<Cine> lcine = Arrays.asList(this.cine);
when(cDao.listAll())
.thenReturn(lcine);
Me creo una lista con un cine, y luego indico que si alguien llama cDao.listAll(), se deberá devolver la lista
A continuacion, llamo al metodo del controlador que es leerTodos(), y recojo su respuesta
ResponseEntity<Map<String, Object>> response = cineController.leerTodos();
Si miramos la codificación del controlador veremos que invoca a cDao.listAll(), de alli, recibira la lista que hemos preparado de Cine, y lo debe convertir en CineDTO para devolverlo
por lo que en nuestro test, deberemos verificar que nos devuelve lo que hemos enviado
// Verificar
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(1, response.getBody().get("status"));
assertTrue(compararListas(Arrays.asList(this.cineDTO), (List<CineDTO>)response.getBody().get("data")));
Si todo va bien, el test pasará
Vamos a ver otro ejemplo, el test de leerUno. Debemos indicar la id, aunque nos da lo mismo, ya que tambien aqui, debemos indicar previamente el comportamiento de cDao
when(cDao.leerUno(1L)).thenReturn(cineOptional);
Si nosotros, ahora le hacemos pa petición al controlador… y pedimos el 1, cDao devolverá la informacion correspondiente
ResponseEntity<Map<String, Object>> response = cineController.leerUno(1L);
// Verificar
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(1, response.getBody().get("status"));
assertTrue(compararCines(cineDTO, (CineDTO)response.getBody().get("data")));
y solo queda comprobarla. Atencion, si nosotros dijeramos leerUno(2L), el controlador lanzaría un cDao.leerUno(2), que no es la peticion prevista, por lo que no funcionaria el test.
Y asi sucesivamente, os invito a que lo intenteis escribir, y si no, podeis acudir a Github para comprobar el resultado. Para hecerlo, teneis que ver la llamada que haceis en el controlador a cDao en cada caso, y los datos que enviais a cDao, y los quenecesitais recibir, para crear la estructura
- when(la llamada que se hara)thenReturn(lo que os interesa recibir) o
- when(cDao.insert(any(Cine.class))).thenThrow(ConstraintViolationException.class)
En el segundo caso, por ejemplo, no me preocupan los valores del objeto Cine que se van a enviar, por lo que puedo utilizar el método any…. y en lugar de un valor, quiero que me devuelva un error (cosa que pasara si hay algun error en la clase Cine)
Este ultimo caso, es el test para comprobar que el controlador sabe manejar errores
@Test
void whenPostErrorNombre_thenReturns400() throws Exception {
when(cDao.insert(any(Cine.class))).thenThrow(ConstraintViolationException.class);
assertThrows(ControllerException.class, () -> cineController.alta(cineController.convertToDto(cineErrorNombre)));
}
Cuando ejecutemos todo el test, ahora, deberíamos obtener algo como esto
En donde vemos que todos los test han pasado.
Conclusión
Acabáis de ver un ejercicio de test que, aunque útil y funcional, deja mucho que desear, y solo lo he utilizado para empezar a mostrar los recursos, por lo que aunque en GitHub lo teneis totalmente resuelto, no seria el mejor test a escribir; ese lo veremos en el siguiente articulo.
De todas formas, os quiero, tambien dejar la realización en video, y, espero que esté listo antes del 25/4.
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
Relacionado
Descubre más desde Recursos para formacion
Suscríbete y recibe las últimas entradas en tu correo electrónico.