En este tutorial aprenderemos a montar un proyecto de cero con Quarkus, a trabajar con el modo de desarrollador, exponer endpoints con RESTEasy, conectar una BBDD con MongoDB, usar el cliente de Panache, hacer test colaborativos y a hacer validaciones.
Índice de contenidos
- 1. Introducción a servicios REST con Quarkus
- 2. Modo de desarrollador con Quarkus
- 3. MongoDB con Panache
- 4. POST con RESTEasy con Jackson
- 5. Como usar PanacheMock para mockear la capa de persistencia
- 6. Validación en Quarkus
- 7. Conclusiones
- Enlaces y referencias
1. Introducción a servicios REST con Quarkus
Queremos crear un microservicio Quarkus que exponga unos endpoints REST para poder realizar un CRUD, y que guarde los datos en una BBDD NoSQL, en este caso he elegido MongoDB.
Lo primero que hacemos es bajarnos una imagen docker de MongoDB.
docker run --name periodic-table-mongo -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=mongo -e MONGO_INITDB_ROOT_PASSWORD=mongo mongo:latest
Para posteriormente, dirigirnos a la página de Quarkus donde seleccionar las librerías que va a necesitar nuestra aplicación.
Vamos a seleccionar:
- RESTEasy JAX-RS para los endpoints
- RESTEasy Jackson para la serialización/deserialización con Jackson
- MongoDB with Panache para la persistencia en MongoDB usando un ORM simplificado
Descargamos el zip que nos sugieren, indicando la paquetería, el nombre del artefacto y la herramienta de compilación, en mi caso con maven, y al descomprimir vemos que tenemos un directorio con la estructura de un proyecto Java, con un pom.xml con las dependencias que le hemos indicado.
El proyecto trae un Resource que expone un endpoint de ejemplo:
package org.autentia.lab; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/hello") public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Hello RESTEasy"; } }
2. Modo de desarrollador con Quarkus
Quarkus tiene una opción muy interesante que es la opción de desarrollador.
Esto nos permite hacer «live coding». Podemos hacer cambios, sin necesidad de recompilar, redesplegar la aplicación y reiniciar cada vez que se cambia código.
Vamos a probarlo.
mvn quarkus:dev
Si abrimos un navegador y vamos a http://localhost:8080/hello podremos ver el mensaje «Hello RESTEasy»
Vale. Comprobemos que podemos cambiar el código sin necesidad de redesplegar:
@GET @Path("/{name}") @Produces(MediaType.TEXT_PLAIN) public String hello(@PathParam("name") String name) { return "Hello " + name; }
Y nos vamos al navegador y ponemos http://localhost:8080/hello/Juan%20Antonio y veremos:
Efectivamente, el endpoint y la respuesta han cambiado. Mientras estemos dentro del modo desarrollador, no tendremos que redesplegar para ir validando los cambios.
Más adelante veremos que es muy potente y que se puede configurar. Por ejemplo, se puede configurar para que pasen los test sólos cada vez que se hace algún cambio. Que no se pasen. O que se pasen sólo aquellos que están afectados por el cambio…
3. MongoDB con Panache
Panache es un ORM muy simplificado sobre Hibernate que cumple en esencia con lo que se necesita de un ORM sin la necesidad de tener que trabajar con el EntityManager. Tiene dos estrategias: «registro activo» o «patrón repositorio». He probado las dos, y la simplicidad que me ofrece Active Record ya no la cambio por nada.
Con MongoDB también podemos usar la estrategia de «Registro Activo» a través de Panache, que en mi opinión es mucho más simple que usar el cliente de Quarkus para MongoDB, que tampoco nos engañemos, es también muy simple. En Quarkus todo es simple.
Lo primero es configurar nuestro MongoDB en el application.properties de nuestro proyecto:
# MongoDB datasource config quarkus.mongodb.connection-string = mongodb://develop.myhost.com:27017 quarkus.mongodb.database = ddbb-name quarkus.mongodb.credentials.username=ddbb-username quarkus.mongodb.credentials.password=ddbb-password %dev.quarkus.mongodb.connection-string = mongodb://localhost:27017 %dev.quarkus.mongodb.database = admin %dev.quarkus.mongodb.credentials.username=mongo %dev.quarkus.mongodb.credentials.password=mongo
El primer bloque es la configuración para el entorno que se despliegue. Aunque si estás desplegando en Cloud seguramente estos valores se sobreescriban con los charts dependiendo de si es el entorno de desarrollo o de producción.
Los mismos nombres de variables precedidos por %dev. se refieren a la configuración para el modo de desarrollo cuando arrancas con mvn quarkus:dev.
Existe también la posibilidad de configurar propiedades para los test precediendo con el prefijo %test.
Bien, pues lo primero vamos a retomar nuestro ejercicio de hacer un CRUD sobre la Tabla Periódica. Para ello vamos a definir nuestra entidad de BBDD.
package org.autentia.lab; import io.quarkus.mongodb.panache.PanacheMongoEntity; public class Element extends PanacheMongoEntity { public String symbol; public String name; public Integer group; public Integer period; public Integer atomicNumber; public Double atomicMass; public String electronConfiguration; public static Element findBySymbol(String symbol) { return find("symbol", symbol).firstResult(); } }
Nuestro elemento tendrá los campos habituales de un elemento de la tabla periódica: un símbolo, el nombre, el grupo y periodo, su número atómico, su masa atómica, y la configuración electrónica del elemento.
Al extender de PanacheMongoEntity veremos que Element tiene un montón de métodos de acceso a BBDD a los que se puede acceder de forma estática.
Así que ya estamos en condiciones de empezar a describir el comportamiento que deseamos para nuestro microservicio empezando por los test.
Como aún no tenemos código, lo ideal sería hacer test unitarios colaborativos que entren hasta la cocina. Pero primero deberíamos añadir una serie de dependencias al pom.xml para poder hacer los test: JUnit 5, Mockito, y Rest Assured.
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5-mockito</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency>
Con esto ya podemos empezar nuestro test y hacer algunas aserciones.
[...] @QuarkusTest class ElementResourceTest { @Test void cuando_POST_de_un_elemento_nuevo_entonces_lo_crea_y_OK_200() { Element hydrogen = createHydrogen(); given() .when() .header("Content-Type", "application/json") .body(hydrogen) .post("/element") .then() .statusCode(200); verify(hydrogen, times(1)).persist(); } }
Lo primero que quiero que observes es la anotación @QuarkusTest encima del nombre de la clase del test.
Evidentemente si corremos el test nos fallará… Aún no tenemos el resource que escucha en «/element» un POST.
Y tampoco llamamos persistimos ningún objeto.
4. POST con RESTEasy con Jackson
Creamos el ElementResource
public class ElementResource { [...] @POST public Response createElement(@Valid Element element) { element.persist(); return Response.ok().build(); } }
Y lanzamos de nuevo el test. Lógicamente nos falla, porque el verify() no puede observar hydrogen al no ser un mock. Comentamos esta línea y volvemos a ejecutar… y ¡funciona!
Pero, ¿cómo puede ser eso? Ahhhh… es que no he salido del modo desarrollador, y está levantado el docker con el MongoDB… Por eso ha funcionado.
Salgo del modo desarrollador, paro MongoDB y vuelvo a lanzar el test. Ahora sí. Ahora falla…
En este caso estamos usando element para dos cosas distintas:
- como payload del servicio REST
- como entidad de BBDD con Panache
En muchas ocasiones nos gustaría tener separadas estas dos responsabilidades y tener un ElementDto y un ElementEntity y usar mapStructs o un mapperService para traducir de uno en otro. Y esta responsabilidad no tenerla en la capa de resource si no en un servicio.
@ApplicationScoped public class ElementService { @Inject public MapperService mapper; public void createElement(ElementDto dto) { ElementEntity entity = mapper.toEntity(dto); entity.persist(); } }
De esta forma, nuestro proyecto ya va tomando forma.
Mockeamos el mapper y lo entrenamos para que cuando transformemos el DTO a una entidad, nos devuelva un spy de dicha entidad.
Además, le decimos que no haga nada cuando vaya a persistir dicha entidad.
De esta forma, en la aserción, además de comprobar que nos devuelve un 200 OK, comprobamos que ha pasado una vez por el método persist()
@InjectMock MapperService mapper; @Test void cuando_POST_de_un_elemento_nuevo_entonces_lo_crea_y_OK_200() { ElementDto hydrogenDto = createHydrogen(); ElementEntity hydrogenEntity = spyHydrogenEntity(); when(mapper.toEntity(any(ElementDto.class))).thenReturn(hydrogenEntity); doNothing().when(hydrogenEntity).persist(); given() .when() .header("Content-Type", "application/json") .body(hydrogenDto) .post(BASE_PATH) .then() .statusCode(200); verify(hydrogenEntity, times(1)).persist(); }
Aunque el test tiene muy buena pinta, se nos ha escapado un matiz. «Elemento nuevo«. En ningún momento verificamos si el elemento ya existe o no.
Aquí, REST, nos da varias posibilidades:
- devolver un 201 OK si el elemento ya existe (POST no es idempotente)
- devolver un error 400, porque el elemento ya se ha dado de alta
Aquí es donde usaríamos el método que creamos al principio findBySymbol()
public Response createElement(ElementDto dto) { if (!exists(dto)) { ElementEntity entity = mapper.toEntity(dto); entity.persist(); return Response.ok().build(); } return Response.status(201).build(); } private boolean exists(ElementDto dto) { return (ElementEntity.findBySymbol(dto.symbol) != null); }
Ahora sí. Buscamos primero si la entidad ya está persistida, y si no, la persistimos.
Pero al lanzar de nuevo el test nos llevamos una sorpresa: falla
¿por qué?
5. Como usar PanacheMock para mockear la capa de persistencia
Es muy habitual usar H2 en los test como BBDD en memoria. En ese caso nunca se va a mockear una entidad. Pero con MongoDB, o nos levantamos un contenedor, o mockeamos la entidad y ahí nos encontramos con que el findBySymbol() es un método estático. Quarkus lo tiene solucionado usando PanacheMock.
Sólo tenemos que añadir la dependencia en el pom.xml
... <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-panache-mock</artifactId> <scope>test</scope> </dependency>
Y en nuestros test indicamos que las entidades son de tipo PanacheMock.
@BeforeEach public void beforeEach() { PanacheMock.mock(ElementEntity.class); }
Y ya estaríamos en condiciones de entrenar el mock en nuestro test
[...] when(ElementEntity.findBySymbol("H")).thenReturn(null); [...]
De esta forma, el siguiente test sale casi automático.
@Test void cuando_POST_de_un_elemento_que_ya_existe_entonces_no_lo_persiste_y_OK_201() { ElementDto hydrogenDto = createHydrogen(); ElementEntity hydrogenEntity = spyHydrogenEntity(); when(ElementEntity.findBySymbol("H")).thenReturn(hydrogenEntity); given() .when() .header("Content-Type", "application/json") .body(hydrogenDto) .post(BASE_PATH) .then() .statusCode(201); verify(mapper,never()).toEntity(any()); verify(hydrogenEntity, never()).persist(); }
6. Validación en Quarkus
Hemos ido a lo más complicado, y hemos dejado de lado las validaciones. Antes de nada debemos comprobar que el DTO que recibimos es válido y está bien formado.
Como todo lo demás, la validación también es muy sencilla en Quarkus. La forma en que lo hace es a través del Hibernate Validator. Sólo debemos incluirlo en el pom.xml
... <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-hibernate-validator</artifactId> </dependency>
Y en el método del resource añadir el @Valid:
@POST public Response createElement(@Valid ElementDto dto) { return service.createElement(dto); }
Ya es suficiente que anotemos las propiedades del DTO con las validaciones que deseemos
public class ElementDto { @NotNull(message = "Symbol cannot be null") @Size(min = 1, max = 2, message = "Symbol is 1 or 2 chars") public String symbol; @NotNull(message = "Name cannot be null") public String name; @NotNull(message = "Group cannot be null") @Min(value = 1, message = "Min group value is 1") @Max(value = 18, message = "Max group value is 18") public Integer group; @NotNull(message = "Period cannot be null") @Min(value = 1, message = "Min period value is 1") @Max(value = 18, message = "Max period value is 18") public Integer period; @NotNull(message = "Atomic Number cannot be null") @Min(value = 1, message = "Min Atomic Number value is 1") @Max(value = 118, message = "Max Atomic Number value is 118") public Integer atomicNumber; @NotNull(message = "Atomic Mass be null") @Min(value = 1, message = "Min Atomic Mass value is 1") @Max(value = 294, message = "Max Atomic Mass value is 294") public Double atomicMass; @NotNull(message = "Electron Configuration cannot be null") public String electronConfiguration; }
Sólo con esto, la validación hace su magia.
@Test void cuando_POST_de_un_elemento_sin_symbol_entonces_error_400() { ElementDto hydrogenDto = createHydrogen(); hydrogenDto.symbol = null; given() .when() .header("Content-Type", "application/json") .body(hydrogenDto) .post(BASE_PATH) .then() .statusCode(400) .body(containsString("Symbol cannot be null")); }
7. Conclusiones
Este artículo ha sido una breve introducción muy básica a Quarkus, pero hemos tocado muchas cositas.
Hemos aprendido a:
- montar un proyecto de cero
- trabajar con el modo de desarrollador de quarkus
- exponer endpoints con RESTEasy
- conectar una BBDD con MongoDB
- usar el cliente de Panache para MongoDB
- empezar a usar test colaborativos con @QuarkusTest
- usar PanacheMock para evitar el acceso a BBDD
- validar las llamadas REST