Testing en aplicaciones con Quarkus
Como ya hemos visto en entradas anteriores, Quarkus es un framework totalmente orientado al cloud. En este artículo vamos a ver como podemos hacer testing en aplicaciones con Quarkus, ya que no podemos olvidarnos de realizar testing en nuestras aplicaciones para evitar errores.
Si quieres ver otros artículos sobre testing en Spring Boot puedes echar un ojo aquí.
Configuración de una aplicación Quarkus para testing
Vamos a partir de una apliación anterior en la que vamos a añadir configuración para hacer un buen testing de nuestra aplicación. Puedes hacer un git clone con esta url, https://github.com/refactorizando-web/quarkus-testing
Este proyecto expone un simple endpoint de entrada a la aplicación con una conexión a una Base de Datos H2. El proyecto se divide en 3 capas que veremos a continuación.
Para poder hacer test sobre nuestro servicio creado en Quarkus vamos a añadir, entre otras, las siguientes dos dependencias:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5-mockito</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-h2</artifactId> </dependency>
Creación de proyecto en Quarkus para testing
Para poder realizar testing en aplicaciones con Quarkus vamos a mostrar una pequeña apliación.
Crear Resource en Quarkus
A continuación vamos a crear un proyecto simple con 3 capas para poder realizar test integrados y unitarios sobre ellos:
@Path("/cars") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class CarResource { @Inject CarService carService; @GET @Path("/model") public Set<CarEntity> findModel(@QueryParam("query") String query) { return carService.find(query); } }
Crear servicio en Quarkus
Vamos a crear la capa Service que será la encargada de realizar la lógica de negocio de nuestra aplicación. Esta capa mediante inyección de dependencias haciendo uso de @Inject se comunicará con el repositorio.
@Transactional @ApplicationScoped public class CarService { @Inject CarRepository carRepository; public Set<CarEntity> find(String query) { if (query == null) { return carRepository.findAll().stream().collect(toSet()); } return carRepository.findBy(query).collect(toSet()); } }
Crear capa Repository en Quarkus
Nuestra capa repository será la que se comunique directamente con nuestra BBDD, en este caso es una base de datos en memoria (H2). Para realizar la búsqueda, vamos a realizar una query a mano en nuestra capa repository. Una de las funcionalidades de Quarkus es que directamente nos va a devolver un Stream.
@ApplicationScoped public class CarRepository implements PanacheRepository<CarEntity> { public Stream<CarEntity> findBy(String query) { return find("color like :query or model like :query", with("query", "%"+query+"%")).stream(); } }
Añadir datos en tiempo de test con @Alternative
Cuando hacemos tests vamos a querer tener datos en nuestra Base de Datos, esta funcionalidad en Quarkus la podemos hacer haciendo uso de @Alternative para proporcionar un bean para cargar datos en nuestra aplicación.
@Priority(1) @Alternative @ApplicationScoped public class TestCarRepository extends CarRepository { @PostConstruct public void init() { persist(new CarEntity("Mustang", "Blue"), new CarEntity("AudiA6", "Grey")); } }
Aunque en nuestro código ya tengamos una implementación del Repository con las anotaciones @Priority(1) y @Alternative en tiempo de test se sobreescribirá con nuestra nueva clase. Estas anotaciones nos pueden ayudar para crear casos de pruebas.
Testing en Quarkus haciendo uso de Mocks
Quarkus nos ofrece la anotación @QuarkusMock para poder hacer mocks en nuestros test. El uso de mock es una de las herramientas más comunes cuando trabajamos con mocks.
Uso de QuarkusMock
La librería de QuarkusMock puede ser usada para simular temporalmente cualquier bean. Al usar este método con @BeforeAll tendrá efecto en todas las clases en cambio, podemos aplicarlo a nivel de método usándolo únicamente en un método.
@QuarkusTest class CarServiceInjectMockUnitTest { @Inject CarService carService; @InjectMock CarRepository carRepository; @BeforeEach void setUp() { when(carRepository.findBy("yellow")) .thenReturn(Arrays.stream(new CarEntity[] { new CarEntity("Megane", "yellow"), new CarEntity("A6", "yellow")})); } @Test void whenGetCarByModel_thenCarsAreReturned() { assertEquals(2, carService.find("yellow").size()); } }
En el ejemplo anterior lo que hacemos es inyectar el mock que hemos definido con @Alternative cuando nuestra clase repository principal es invocada.
Uso de InjectMock en nuestros test con Quarkus
Una de las funcionalidades que nos ofrece Mockito es hacer uso de una «simulación» de inyección de dependencias haciendo uso de @InjectMock, de manera que en lugar de usar QuarkusMock hacemos uso de la anotación @InjectMock y tendríamos nuestra clase inyectada en el contexto de test.
@QuarkusTest class CarServiceQuarkusMockUnitTest { @Inject CarService carService; @BeforeEach void setUp() { CarRepository mock = Mockito.mock(TestCarRepository.class); Mockito.when(mock.findBy("grey")) .thenReturn(Arrays.stream(new CarEntity[] { new CarEntity("A6", "grey"), new CarEntity("I30", "grey"), new CarEntity("Toledo", "grey")})); QuarkusMock.installMockForType(mock, CarRepository.class); } @Test void whenFindByModel_thenCarsAreReturned() { assertEquals(3, carService.find("grey").size()); } }
Usos de Spy con Quarkus
Cuando hacemos uso de mock lo que hacemos es sustituir los objetos por completo, en cambio con Spy lo que hacemos es mantener los objetos originales y reemplarzar algunos métodos. Para aplicar esta funcionalidad hacemos uso de @InjectSpy.
@QuarkusTest class CarSpyIntegrationTest { @InjectSpy CarService carService; @Test void whenGetCarsByModel_thenModelIsReturned() { given().contentType(ContentType.JSON).param("query", "Mustang") .when().get("/cars/model") .then().statusCode(200); verify(carService).find("Mustang"); } }
Test integrados con Quarkus
Quarkus nos proporciona la anotación @QuarkusTest, para inyectar el contexto en la parte de testing. Esta anotación deberá ir en la parte superior de la clase.
@QuarkusTest class CarResourceIntegrationTest { @Test void whenGetCarsByModel_thenCarsAreReturned() { given().contentType(ContentType.JSON).param("query", "Mustang") .when().get("/cars/model") .then().statusCode(200) .body("size()", is(1)) .body("model", hasItem("Mustang")) .body("color", hasItem("Blue")); } }
Con @QuarkusTest vamos a incorporar rest-assured como una manera conveniente de hacer testing sobre endpoints http.
Uso de @TestHTTPResource para inyectar URLs
Cuando queremos hacer un test integrado debemos invocar a un endpoint en concreto. Esto, lo podemos hacer definiendo el endpoint como una constante o haciendo uso de @TestHTTPResource para inyectar la URL.
@TestHTTPResource("/cars/model") URL carEndpoint;
Test con @TestHTTPEndpoint
@TestHTTPEndpoint nos va a a permitir mantener la url de nuestro servicio, es decir, aunque cambiemos el path de nuestra clase, no necesitaremos hacer cambios en nuestros tests.
@TestHTTPEndpoint(CarResource.class) @TestHTTPResource("model") URL carEndpoint;
Testing en Quarkus con Inject
Quarkus nos va a permitir hacer inyección de dependencias en nuestros test a través de la anotación @Inject.
Uso de pérfiles para Test
Al igual que otros frameworks, por ejemplo con Spring Boot, podemos realizar la configuración de perfiles para usarlos en nuestros test.
Para realizar la configuración y uso de perfiles para test, vamos a implementar la clase QuarkusTestProfile.
Para comenzar definimos nuestra clase que va a hacer uso de QuarkusTestProfile:
public class TestProfileCustom implements QuarkusTestProfile { @Override public Map<String, String> getConfigOverrides() { return Collections.singletonMap("quarkus.resteasy.path", "/custom"); } @Override public Set<Class<?>> getEnabledAlternatives() { return Collections.singleton(TestCarRepository.class); } @Override public String getConfigProfile() { return "custom-profile"; } }
La clase anterior va a sobreescribir los métodos dados por la clase QuarkusTestProfile. En la que indicamos las soluciones alternativas y la configuración del perfil en donde lo vamos a realizar.
Una vez hemos definido nuestra clase anterior tendremos que realizar la definición en nuestro fichero de propiedades para indicar que acción tomar en el perfil que acabamos de crear.
%custom-profile.quarkus.datasource.jdbc.url = jdbc:h2:file:./testdb
@QuarkusTest @TestProfile(TestProfileCustom.class) class CarResourceITest { public static final String CAR_WORKSHOP = "/custom/cars/model"; @Test void whenGetCars_thenAllCarsAreReturned() { given().contentType(ContentType.JSON) .when().get(CAR_WORKSHOP) .then().statusCode(200) .body("size()", is(2)) .body("model", hasItems("AudiA6", "Mustang")); } }
Testing en Quarkus de imagenes nativas
Ya que Quarkus es un framework Cloud Native y orientado a imágenes nativas, lo normal sería poder hacer testing sobre este tipo de imágenes.
Para poder utilizar imágenes nativas con Quarkus vamos a hacer uso de la anotación @NativeImageTest
@NativeImageTest @QuarkusTestResource(H2DatabaseTestResource.class) class NativeLibraryResourceIT extends LibraryHttpEndpointIntegrationTest { }
La anotación @QuarkusTestResource le indica a Quarkus que realice las operaciones necesarias para hacer uso de la imagen nativa.
Para poder construir la imagen y comenzar nuestros test vamos a hacer uso del Profile native:
mvn verify -Pnative
Hay que tener en cuenta que actualmente solo funciona de manera nativa lo que es la aplicación, las inyecciones no funcionan en modo nativo.
Conclusión
En esta entrada hemos visto como hacer testing en aplicaciones con Quarkus, algo esencial cuando creamos aplicaciones o servicios. Crear test es algo esencial e imprescindible para asegurar nuestro código aportando pruebas de su funcionamiento y asegurando las funcionalidades.
Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com o también nos puedes contactar por nuestras redes sociales Facebook o twitter y te ayudaremos encantados!