Seamos sinceros, a nadie le gusta escribir tests. Son aburridos, tediosos, lentos, y siempre hay algo más urgente que hacer. Además, en arquitectura de microservicios, implica preparar mocks o tener servicios externos funcionando, lo que consume tiempo que preferirías dedicar a escribir «código de verdad».
En Quarkus, escribir tests es diferente. Los tests corren solos mientras desarrollas y te avisan al instante si algo se rompe, en lugar de enterarte veinte minutos después cuando falla el pipeline de CI. Los servicios externos se levantan automáticamente. No necesitas depender de servicios mock online ni de aplicaciones dummy locales para simular APIs.
En esta guía vamos paso a paso, desde lo más básico hasta técnicas En esta guía recorreremos paso a paso, desde lo más básico hasta técnicas avanzadas de testing que ofrece Quarkus:
- Tu primer test con
@QuarkusTest - Configuración con perfiles de test
- Continuous Testing
- Dev Services
- REST-assured para endpoints HTTP
- Mocks para servicios externos
- Tests de integración para JAR y binarios nativos
- Tests ultraligeros con
@QuarkusComponentTest
Al final, descubrirás que escribir tests puede convertirse en una de las partes más simples —y útiles— de tu día en el Quarkiverso.
El escenario: carrito de compras
Imagina una tienda online donde los clientes agregan productos al carrito y, al confirmar el pedido, tu servicio consulta el stock a un microservicio de inventario para asegurar disponibilidad.
Este escenario tiene todo lo que necesitamos:
- Lógica de negocio que validar
- Persistencia en una base de datos
- Una llamada a otro microservicio
Vamos a construirlo y testearlo paso a paso.
Requisitos
- Java 17 ó 21 (soportadas por Quarkus)
- Maven 3.9 o superior
- Podman o Docker funcionando
- Quarkus CLI
¿No cumples todos los requisitos? Revisa Entorno de desarrollo ideal para Quarkus donde explicamos cómo instalar todo fácilmente.
Si usas Podman, te sugerimos revisar Usando Podman con Quarkus: la configuración esencial
El proyecto base
Comencemos creando un proyecto con las extensiones necesarias:
quarkus create app io.quarkiverso:testing-quarkus \
--extension=rest-jackson,hibernate-orm-panache,jdbc-postgresql,rest-client-jacksonCuando creas un proyecto Quarkus, el entorno de pruebas queda configurado desde el inicio: se incluye la extensión quarkus-junit5 para integrar JUnit 5 con el runtime de Quarkus, se asegura el uso de JBoss Log Manager —el sistema de logging estándar de Quarkus— y se fija una configuración específica del Maven Surefire Plugin que garantiza una ejecución predecible de las pruebas.
Para las funcionalidades avanzadas de testing que veremos (mocks, PanacheMock, tests de componentes), agregamos estas dependencias al pom.xml:
<!-- REST-assured -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<!-- Para @InjectMock y Mockito -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<!-- Para PanacheMock -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-panache-mock</artifactId>
<scope>test</scope>
</dependency>
<!-- Para @QuarkusComponentTest -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-component</artifactId>
<scope>test</scope>
</dependency>⚠️ No todas las dependencias con prefijo
quarkus-son extensiones de Quarkus en el sentido estricto del framework. Artefactos comoquarkus-junit5-mockito,quarkus-panache-mockoquarkus-junit5-componentson utilidades de testing que integran Quarkus con JUnit o Mockito, pero no participan del build-time ni del runtime de la aplicación.Por este motivo no aparecen en el catálogo de extensiones ni pueden agregarse con
quarkus add extension, y deben declararse explícitamente como dependencias de test.
@QuarkusTest: tu primer test
Empecemos con lo más básico: una clase gestionada por Quarkus que puedes inyectar en otras clases con @Inject.
En nuestra tienda, antes de confirmar un pedido necesitamos validar que las cantidades sean correctas: no pueden ser cero, ni negativas, ni excesivas.
Crea src/main/java/io/quarkiverso/ValidadorPedido.java:
package io.quarkiverso;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ValidadorPedido {
public void validar(int cantidad) {
if (cantidad <= 0) {
throw new IllegalArgumentException("La cantidad debe ser mayor a 0");
}
if (cantidad > 100) {
throw new IllegalArgumentException("No puedes pedir más de 100 unidades");
}
}
}Ahora el creamos el test en src/test/java/io/quarkiverso/ValidadorPedidoTest.java:
package io.quarkiverso;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
// Importa los métodos de aserción de JUnit 5
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
class ValidadorPedidoTest {
// Quarkus inyecta el bean real, igual que en tu aplicación
@Inject
ValidadorPedido validador;
@Test
void cantidadValida() {
// assertDoesNotThrow verifica que NO se lance excepción
assertDoesNotThrow(() -> validador.validar(5));
}
@Test
void cantidadCero() {
// assertThrows verifica que SÍ se lance la excepción esperada
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> validador.validar(0)
);
// assertTrue verifica que la condición sea verdadera
assertTrue(ex.getMessage().contains("mayor a 0"));
}
@Test
void cantidadExcesiva() {
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> validador.validar(150)
);
assertTrue(ex.getMessage().contains("más de 100"));
}
}¿Qué está pasando aquí?
@QuarkusTestle indica a JUnit que los tests deben ejecutarse dentro de un runtime Quarkus inicializado, arrancando la aplicación con su contenedor CDI (Arc), su configuración y servicios tal como lo haría en ejecución real.@Injectinyecta el bean real, exactamente igual que en tu aplicación. No es un mock ni una copia: es elValidadorPedidoreal.@Testmarca cada método como un test individual.assertDoesNotThrow,assertThrows,assertTrue,assertEqualsson métodos de JUnit 5 para verificar comportamiento. Si la verificación falla, el test falla.
Ejecuta los tests:
quarkus testY verás
All 3 tests are passing (0 skipped)💡 ¿Qué hace
quarkus test? A diferencia de./mvnw test, que ejecuta las pruebas una sola vez y finaliza el proceso, quarkus test inicia un runtime de pruebas persistente con Continuous Testing activado: permanece atento a los cambios en el código y vuelve a ejecutar los tests de forma automática.
🤔 ¿Qué diferencia hay entre
quarkus devyquarkus test?quarkus deves la nave en vuelo con todos los sistemas activos, mientras quequarkus testes el simulador de misión, enfocado exclusivamente en validar que cada componente responda como se espera antes del lanzamiento. Por ejemplo, con quarkus test no se exponen endpoints HTTP, a diferencia de lo que ocurre enquarkus dev.
Test Profile: configuración específica para tests
¿Alguna vez quisiste que tu aplicación se comporte diferente en desarrollo, en tests y en producción? Por ejemplo, logs más verbosos mientras desarrollas, pero silenciosos en producción.
Quarkus resuelve esto de manera nativa mediante perfiles de configuración. Cuando se ejecutan pruebas, Quarkus activa automáticamente el perfil test. Solo tienes que usar el prefijo %test en application.properties:
Configura src/main/resources/application.properties:
# Solo en tests
%test.quarkus.log.level=WARN
%test.quarkus.hibernate-orm.database.generation=drop-and-create
# Solo en producción
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://prod-server:5432/tienda
Los perfiles predeterminados en Quarkus son:
%dev→ cuando la aplicación se ejecuta en modo desarrollo (quarkus dev)%test→ cuando se ejecutan pruebas (mvn test,quarkus test)%prod→ cuando la aplicación se ejecuta como artefacto empaquetado (JAR o binario nativo)
Continuous Testing: el guardián silencioso
Aquí es donde Quarkus realmente brilla. En lugar de ejecutar los tests manualmente cada vez que modificas el código, Quarkus puede ejecutarlos de forma automática mientras desarrollas. Es como tener un compañero atento que te avisa al instante: “acabas de romper algo”, apenas guardas el archivo.
quarkus devUna vez que la aplicación arranca, presiona r para iniciar el modo de Continuous Testing:
Press [r] to resume testingEn la terminal verás algo similar a:
All 3 tests are passing (0 skipped)Ahora probemos romper algo a propósito. Abre ValidadorPedido.java, cambia el límite de 100 a 50 en el if y el mensaje, y guarda el archivo.
if (cantidad > 50) {
throw new IllegalArgumentException("No puedes pedir más de 50 unidades");
}Observa la terminal:
1 test failed (2 passing, 0 skipped)El test falla de forma inmediata. Quarkus detecta el cambio y vuelve a ejecutar únicamente los tests afectados, sin reiniciar la JVM ni relanzar el proceso completo. Este feedback casi instantáneo cambia por completo la experiencia de desarrollo.
Deshaz el cambio, guarda nuevamente, y verás cómo los tests vuelven a pasar.
Finalmente, detén la aplicación presionando q.
Dev Services: la infraestructura que aparece sola
Ahora viene la magia. Vamos a guardar productos y pedidos en PostgreSQL.
En un enfoque tradicional, esto implicaría instalar PostgreSQL, crear una base de datos, definir usuarios y contraseñas, configurar la URL de conexión y cruzar los dedos para que todo funcione igual en la máquina de cada integrante del equipo.
En Quarkus, no hacemos nada de eso. Simplemente escribimos el código.
Gracias a Dev Services, Quarkus detecta que la aplicación necesita una base de datos y levanta automáticamente una instancia de PostgreSQL lista para usar en Podman o Docker, tanto en desarrollo como en test, sin configuración manual y de forma efímera.
PostgreSQL es solo un ejemplo: Dev Services puede aprovisionar distintos servicios externos de forma automática como mostramos en el artículo Dev Services: entornos de desarrollo automáticos en Quarkus.
Vamos a crear nuestras entidades. Para eso usaremos Panache, el modelo simplificado de Quarkus para trabajar con JPA. En lugar de escribir repositories, DAOs y código repetitivo, extendemos PanacheEntity y obtenemos métodos estáticos como persist(), listAll(), find(). Ni siquiera es necesario declarar getters y setters.
Crea src/main/java/io/quarkiverso/Producto.java:
package io.quarkiverso;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class Producto extends PanacheEntity {
// PanacheEntity ya incluye el campo 'id' automáticamente
public String codigo;
public String nombre;
public int precio;
// Método personalizado para buscar por código (patrón Active Record)
public static Producto findByCodigo(String codigo) {
return find("codigo", codigo).firstResult();
}
}Crea src/main/java/io/quarkiverso/Pedido.java:
package io.quarkiverso;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import java.time.LocalDateTime;
@Entity
public class Pedido extends PanacheEntity {
@ManyToOne
public Producto producto;
public int cantidad;
public String cliente;
public LocalDateTime fecha = LocalDateTime.now(); // Fecha automática
}Ahora creamos un endpoint REST para exponer los pedidos. Este será el punto de entrada HTTP que luego testearemos:
Crea src/main/java/io/quarkiverso/PedidoResource.java:
package io.quarkiverso;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Path("/pedidos")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PedidoResource {
@GET
public List<Pedido> listar() {
return Pedido.listAll();
}
@POST
@Transactional
public Pedido crear(Pedido pedido) {
pedido.persist();
return pedido;
}
}
Crea el test src/test/java/io/quarkiverso/ProductoTest.java:
package io.quarkiverso;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.transaction.Transactional;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
class ProductoTest {
@Test
@Transactional
void crearProducto() {
Producto p = new Producto();
p.codigo = "LAPTOP-001";
p.nombre = "Laptop Gamer";
p.precio = 1500;
p.persist();
// Verifica que se guardó en la DB real
assertNotNull(p.id);
// Busca el producto que acabamos de guardar usando nuestro método
Producto encontrado = Producto.findByCodigo("LAPTOP-001");
assertEquals("Laptop Gamer", encontrado.nombre);
}
}
Ejecuta el test:
quarkus testy verás:
All 4 tests are passing (0 skipped)Detén la aplicación presionando q.
¿Funcionó sin configurar PostgreSQL?
Sí, y esto es importante entenderlo. Quarkus detectó que tu aplicación utiliza jdbc-postgresql, observó que no había una URL de conexión configurada para el perfil test y tomó una decisión inteligente: levantó automáticamente un contenedor de PostgreSQL mediante Testcontainers, configuró la conexión por tú y, una vez finalizados los tests, lo detuvo de forma limpia.
Esto es Dev Services. No es magia; es Quarkus siendo práctico.
Lo mejor es que el test se ejecuta contra PostgreSQL real, no contra un mock de base de datos. Si una consulta SQL tiene un error de sintaxis, el test falla. Si el mapeo JPA es incorrecto, el test falla. Estás validando el comportamiento real del sistema, no una simulación.
REST-assured: Tests HTTP que se leen como prosa
Hasta ahora probamos lógica interna inyectando dependencias con @Inject. Pero en una API REST también es fundamental verificar que los endpoints HTTP respondan correctamente: códigos de estado, headers y payloads.
Quarkus integra REST-assured de forma nativa, lo que permite escribir tests HTTP con una sintaxis clara y expresiva, casi narrativa. El flujo es natural: dado un contexto, cuando realizo una request, entonces espero una respuesta específica.
Crea src/test/java/io/quarkiverso/PedidoResourceTest.java:
package io.quarkiverso;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.*;
@QuarkusTest
class PedidoResourceTest {
@Test
void listarPedidos() {
given()
.when().get("/pedidos")
.then()
.statusCode(200);
}
@Test
void listarPedidosVacio() {
given()
.when().get("/pedidos")
.then()
.statusCode(200)
.body("size()", equalTo(0)); // Lista vacía
}
}
Ejecuta los tests y verás:
All 6 tests are passing (0 skipped)Sintaxis REST-assured:
REST-assured estructura los tests siguiendo un flujo muy natural:
given()define el contexto inicial de la request, como headers, body o parámetroswhen()ejecuta la acción HTTP (GET, POST, PUT, DELETE)then()valida el resultado, como el código de estado o el contenido de la respuesta
💡 ¿Qué puerto usa? En modo test, Quarkus levanta la aplicación en el puerto 8081, distinto del 8080 usado en desarrollo. REST-assured ya viene configurado para apuntar automáticamente a ese puerto, por lo que no es necesario ajustarlo manualmente.
@TestHTTPEndpoint: Evita repetir paths
¿Notaste que en los tests HTTP acabamos de repetir la ruta "/pedidos" una y otra vez? Si mañana cambia el endpoint a "/api/pedidos", tendrías que actualizar todos esos tests manualmente.
Pero Quarkus ofrece una alternativa más limpia y mantenible. Modifica src/test/java/io/quarkiverso/PedidoResourceTest.java agregando @TestHTTPEndpoint y eliminando las rutas en los métodos get() de la siguiente manera:
package io.quarkiverso;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.*;
@QuarkusTest
@TestHTTPEndpoint(PedidoResource.class)
class PedidoResourceTest {
@Test
void listarPedidos() {
given()
.when().get()
.then()
.statusCode(200);
}
@Test
void listarPedidosVacio() {
given()
.when().get()
.then()
.statusCode(200)
.body("size()", equalTo(0)); // Lista vacía
}
}
Si mañana cambias @Path("/pedidos") por @Path("/api/pedidos"), los tests siguen funcionando. El contrato está en el recurso, no duplicado en cada test.
@InjectMock: mocks para servicios externos
Aquí viene un punto clave. En el mundo real, un servicio no vive aislado: suele depender de otros servicios. En nuestro caso, antes de confirmar un pedido, necesitamos consultar el stock en otro microservicio de inventario.
El problema clásico: ¿cómo pruebas este flujo sin tener el servicio de inventario realmente corriendo?
Muchos equipos recurren a soluciones frágiles.
- Servicios mock online como mocky.io, mockapi.io o beeceptor. Funcionan… hasta que el servicio no responde, no hay conexión a Internet, alguien cambia la respuesta o se alcanza el límite del plan gratuito.
- Aplicaciones «dummy» locales: Se crea un proyecto separado que simula el servicio real. Con el tiempo, nadie recuerda por qué responde lo que responde, se desincroniza del servicio real y hay que recordar levantarlo antes de ejecutar los tests.
En Quarkus, hay una alternativa mucho más sólida: el mock vive en el código, versionado junto con los tests y siempre disponible en el entorno de pruebas.
Primero definimos el cliente REST real que usará nuestra aplicación. En Quarkus, esto se expresa como una interfaz que describe cómo comunicarse con el servicio externo. No escribimos la implementación: Quarkus la genera automáticamente en tiempo de ejecución.
Crea src/main/java/io/quarkiverso/InventarioClient.java:
package io.quarkiverso;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@Path("/api/stock")
@RegisterRestClient(configKey = "inventario") // Nombre para la config
public interface InventarioClient {
// GET /api/stock?codigo=LAPTOP-001 → devuelve un int
@GET
int consultarStock(@QueryParam("codigo") String codigoProducto);
}
¿Solo una interfaz? Sí. Quarkus genera la implementación del cliente REST automáticamente en tiempo de compilación, a partir de la interfaz y su configuración. Solo es necesario definir la URL base del servicio.
Agrega a application.properties:
quarkus.rest-client.inventario.url=http://inventario-service:8080Ahora pasemos al servicio que orquesta el flujo completo: valida la cantidad solicitada, consulta el stock en el servicio de inventario y crea el pedido.
Crea src/main/java/io/quarkiverso/PedidoService.java:
package io.quarkiverso;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.eclipse.microprofile.rest.client.inject.RestClient;
@ApplicationScoped
public class PedidoService {
@Inject
ValidadorPedido validador; // El validador que creamos antes
@Inject
@RestClient
InventarioClient inventario; // El cliente REST al servicio externo
@Transactional
public Pedido crear(String codigoProducto, int cantidad, String cliente) {
// Paso 1: Validar que la cantidad sea correcta
validador.validar(cantidad);
// Paso 2: Consultar stock en el microservicio de inventario
// ¡Esta es la llamada HTTP real al servicio externo!
int stockDisponible = inventario.consultarStock(codigoProducto);
// Paso 3: Verificar que hay suficiente stock
if (stockDisponible < cantidad) {
throw new IllegalStateException(
"Stock insuficiente. Disponible: " + stockDisponible);
}
// Paso 4: Buscar el producto en nuestra base de datos
Producto producto = Producto.findByCodigo(codigoProducto);
if (producto == null) {
throw new IllegalArgumentException("Producto no encontrado: " + codigoProducto);
}
// Paso 5: Crear y guardar el pedido
Pedido pedido = new Pedido();
pedido.producto = producto;
pedido.cantidad = cantidad;
pedido.cliente = cliente;
pedido.persist();
return pedido;
}
}
Ahora crearemos el test sin depender de ningún servicio externo. Aquí está la magia de @InjectMock:
Crea src/test/java/io/quarkiverso/PedidoServiceTest.java:
package io.quarkiverso;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
// Importa when(), verify(), anyString(), etc.
import static org.mockito.Mockito.*;
@QuarkusTest
class PedidoServiceTest {
@Inject
PedidoService service; // El servicio REAL que queremos testear
@InjectMock
@RestClient
InventarioClient inventarioMock; // El cliente REST es REEMPLAZADO por un mock
@BeforeEach // Se ejecuta ANTES de cada test
@Transactional
void setup() {
// Limpiamos la base de datos y creamos un producto de prueba
// Primero Pedido (tiene FK a Producto), luego Producto
Pedido.deleteAll();
Producto.deleteAll();
Producto p = new Producto();
p.codigo = "LAPTOP-001";
p.nombre = "Laptop Gamer";
p.precio = 1500;
p.persist();
}
@Test
void pedidoConStock() {
// Configuramos el mock
// "Cuando alguien llame a consultarStock con 'LAPTOP-001', devuelve 10"
when(inventarioMock.consultarStock("LAPTOP-001")).thenReturn(10);
// Ejecutamos el código que queremos testear
Pedido pedido = service.crear("LAPTOP-001", 2, "cliente@email.com");
// Verificamos que todo salió bien
assertNotNull(pedido.id); // El pedido se guardó (tiene ID)
assertEquals(2, pedido.cantidad); // La cantidad es correcta
// Confirmamos que el mock fue llamado correctamente
verify(inventarioMock).consultarStock("LAPTOP-001");
}
@Test
void pedidoSinStock() {
// El mock devuelve solo 1 unidad disponible
when(inventarioMock.consultarStock("LAPTOP-001")).thenReturn(1);
// Intentamos pedir 5 unidades - debe fallar
IllegalStateException ex = assertThrows(
IllegalStateException.class,
() -> service.crear("LAPTOP-001", 5, "cliente@email.com")
);
// Verificamos que el mensaje de error es el esperado
assertTrue(ex.getMessage().contains("Stock insuficiente"));
}
}
El resultado:
All 8 tests are passing (0 skipped)¿Qué sucedió?
La clave está en @InjectMock y @RestClient: Quarkus detecta que el test necesita inyectar InventarioClient y, en lugar de crear el cliente REST real —que realizaría llamadas HTTP—, inyecta un mock de Mockito.
Dentro del test:
when(...).thenReturn(...)define el comportamiento esperado del servicio externoverify(...)permite confirmar que el servicio fue invocado, asegurando que el flujo realmente lo utiliza
El resultado es un test que valida el flujo completo sin Internet, sin servicios externos y sin infraestructura adicional. Y si mañana cambia la URL del servicio de inventario, los tests siguen funcionando sin modificaciones porque inyecta el cliente y no hardcodea la ruta del microservicio inventario.
Mockito: Simulando el caos
Hasta ahora simulamos respuestas exitosas, pero en el mundo real los servicios fallan. ¿Qué ocurre si el servicio de inventario no responde? ¿La aplicación falla de forma controlada o colapsa?
Con Mockito, estos escenarios se pueden reproducir de forma precisa y predecible dentro de un test, sin necesidad de romper nada en producción.
Agreguemos el siguiente método en src/test/java/io/quarkiverso/PedidoServiceTest.java:
@Test
void pedidoInventarioNoDisponible() {
// thenThrow simula que el servicio lanza una excepción
// Esto representa: servicio caído, timeout, error de red, etc.
when(inventarioMock.consultarStock(anyString()))
.thenThrow(new jakarta.ws.rs.ProcessingException("Connection refused"));
// Verificamos que nuestra app propaga la excepción correctamente
// (en producción querrías manejar esto con un try-catch y mensaje amigable)
assertThrows(
jakarta.ws.rs.ProcessingException.class,
() -> service.crear("LAPTOP-001", 1, "cliente@email.com")
);
}
Verás:
All 9 tests are passing (0 skipped)¿Por qué es importante testear estos escenarios? Porque “funciona cuando todo sale bien” no es suficiente. También necesitás saber cómo se comporta la aplicación cuando las cosas salen mal: si falla de forma controlada, si devuelve un error claro o si degrada su funcionalidad de manera predecible. Para este tipo de pruebas, Mockito ofrece herramientas muy útiles.
Matchers útiles de Mockito: En muchos casos no importa el valor exacto de un parámetro, sino simplemente que el método sea invocado. Para eso existen los matchers, que permiten definir comportamientos más flexibles en los mocks:
// anyString(): acepta cualquier string como parámetro
when(mock.consultarStock(anyString())).thenReturn(10);
// any(): acepta cualquier objeto
when(mock.procesar(any())).thenReturn(resultado);
// times(n): verifica que se llamó exactamente n veces
verify(mock, times(2)).consultarStock(anyString());
// never(): verifica que nunca se llamó (útil para verificar que algo NO pasó)
verify(mock, never()).consultarStock("PRODUCTO-INVALIDO");
Con estas herramientas, el caos deja de ser impredecible. Podés simular escenarios como servicios caídos, timeouts, respuestas vacías o errores de red, y verificar que tu aplicación los maneja de forma controlada y consistente.
PanacheMock: Tests sin base de datos
¿Recordás Dev Services levantando PostgreSQL de forma automática? Es ideal para tests de integración, donde quieres validar el comportamiento real contra una base de datos. Pero no siempre eso es necesario. A veces estás probando lógica pura y buscás tests más rápidos y aislados.
Para esos casos existe PanacheMock, que permite simular los métodos estáticos de Panache sin tocar una base de datos real.
Crea src/test/java/io/quarkiverso/ProductoMockTest.java:
package io.quarkiverso;
import io.quarkus.panache.mock.PanacheMock;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@QuarkusTest
class ProductoMockTest {
@BeforeAll
static void setup() {
// PanacheMock.mock() intercepta los métodos estáticos de Panache
PanacheMock.mock(Producto.class);
}
@Test
void testBuscarProductoPorCodigo() {
// Creamos el objeto que el mock devolverá
Producto productoFalso = new Producto();
productoFalso.codigo = "TEST-001";
productoFalso.nombre = "Producto de prueba";
// Configuramos que cuando llamen a findByCodigo("TEST-001"), devuelve productoFalso
when(Producto.findByCodigo("TEST-001")).thenReturn(productoFalso);
// Ahora esta llamada NO toca la base de datos
Producto encontrado = Producto.findByCodigo("TEST-001");
// El assertEquals confirma que el mock funcionó correctamente
assertEquals("Producto de prueba", encontrado.nombre);
assertEquals("TEST-001", encontrado.codigo);
}
}
Y verás:
All 10 tests are passing (0 skipped)⚠️ Cuándo usar PanacheMock: úsalo solo en tests unitarios muy rápidos, donde quieres aislar lógica de negocio sin depender de infraestructura. Para la mayoría de los casos, Dev Services con PostgreSQL real ofrece mayor confianza y fidelidad al comportamiento del sistema.
@QuarkusIntegrationTest: el test final
Hasta ahora usamos @QuarkusTest, donde Quarkus se ejecuta dentro del mismo proceso que los tests. Esto es rápido y muy conveniente durante el desarrollo cotidiano.
Sin embargo, existe una diferencia importante entre “funciona en desarrollo” y “funciona cuando lo empaqueto”. El artefacto final puede incorporar dependencias distintas, configuraciones exclusivas de producción o comportamientos que solo aparecen cuando la aplicación se ejecuta como un proceso independiente.
Ahí es donde entra @QuarkusIntegrationTest. Este tipo de test arranca la aplicación empaquetada —ya sea como JAR o como ejecutable nativo— en un proceso separado, y luego ejecuta pruebas contra ella como si fuera un sistema externo.
Es como si ejecutaras java -jar tu-app.jar y luego hicieras peticiones HTTP.
Crea src/test/java/io/quarkiverso/PedidoIT.java:
package io.quarkiverso;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
// @QuarkusIntegrationTest = la app corre en un proceso SEPARADO
@QuarkusIntegrationTest
class PedidoIT { // Nombre termina en IT (Integration Test) por convención
@Test
void endpointResponde() {
// Este test hace una petición HTTP REAL al artefacto empaquetado
given()
.when().get("/pedidos")
.then()
.statusCode(200);
}
}
Por defecto, los tests de integración construyen y ejecutan el artefacto usando el perfil prod.
En este artículo, queremos ejecutar los tests con la configuración de test (por ejemplo, base de datos de prueba o logging distinto) sin contaminar el binario final. Para eso, configuramos el perfil con el que se ejecuta el artefacto durante los tests agregando la siguiente propiedad en application.properties:
quarkus.test.integration-test-profile=testDe este modo, el artefacto:
- se construye con el perfil
prod - pero se ejecuta con el perfil
testdurante los tests de integración, aplicando las propiedades%test.*
Esto asegura que estás validando el comportamiento real del artefacto, pero en un entorno de test controlado.
Cómo se ejecutan los tests de integración
# Compila el JAR y ejecuta los @QuarkusIntegrationTest contra el JAR
./mvnw verify -DskipITs=false
# Compila el binario nativo y ejecuta los @QuarkusIntegrationTest contra el ejecutable nativo
./mvnw verify -DnativeTestear el ejecutable nativo
Cuando compilas tu aplicación como binario nativo con GraalVM o Mandrel, obtienes un ejecutable con arranque casi instantáneo y consumo de memoria reducido. Sin embargo, el proceso de compilación nativa es distinto al del JAR: elimina código no alcanzable, resuelve reflexión en build time y puede tener comportamientos que no aparecen en modo JVM.
Por eso es fundamental ejecutar también los tests de integración contra el binario nativo. Si los tests pasan, tienes una garantía fuerte de que el ejecutable que irá a producción funciona correctamente.
@QuarkusComponentTest: tests ultraligeros
A veces no necesitás levantar todo Quarkus. Quizás estás probando un servicio que encapsula lógica de negocio, hace cálculos o aplica reglas, y no interactúa con bases de datos ni expone endpoints HTTP.
Para estos casos, Quarkus 3.2 incorporó @QuarkusComponentTest. Este tipo de test inicia únicamente el contenedor CDI (Arc), sin capa REST, sin base de datos y sin Dev Services. El resultado es un entorno de pruebas mucho más liviano y rápido que @QuarkusTest:
Crea src/test/java/io/quarkiverso/ValidadorComponentTest.java:
package io.quarkiverso;
import io.quarkus.test.component.QuarkusComponentTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
// Solo levanta el contenedor CDI, nada más
// No hay REST, no hay base de datos, no hay Dev Services
@QuarkusComponentTest
class ValidadorComponentTest {
@Inject
ValidadorPedido validador;
@Test
void testLogica() {
// Test ultra-rápido de lógica pura
assertDoesNotThrow(() -> validador.validar(5));
}
}
Luego de correr las pruebas, verás:
All 11 tests are passing (0 skipped)¿Cuándo usar @QuarkusComponentTest? Cuando tienes beans que encapsulan lógica pura —cálculos, validaciones o transformaciones— sin dependencias de infraestructura. Es el tipo de test más liviano y rápido del Quarkiverso.
Resumen: ¿qué herramienta usar en cada caso?
Después de todo lo que vimos, aquí tienes una guía rápida para elegir el tipo de test adecuado según el escenario.
¿Qué necesito testear?
│
├─► Lógica de negocio (un bean CDI)
│ └─► @QuarkusTest + @Inject
│ El bean real, inyectado como en ejecución normal
│
├─► Persistencia con base de datos
│ └─► Dev Services
│ Base de datos real (PostgreSQL), sin configuración manual
│
├─► Tests rápidos sin base de datos
│ └─► PanacheMock
│ Simula los métodos de Panache
│
├─► Endpoints HTTP
│ └─► REST-assured
│ given().when().then()
│
├─► Llamadas a servicios externos
│ └─► @InjectMock @RestClient
│ when(mock.metodo()).thenReturn(valor)
│
├─► Errores de red o servicios caídos
│ └─► Mockito
│ when(mock.metodo()).thenThrow(excepcion)
│
├─► El artefacto empaquetado (JAR o nativo)
│ └─► @QuarkusIntegrationTest
│ Proceso separado, como en producción
│
└─► Solo CDI, nada más
└─► @QuarkusComponentTest
El test más liviano y rápido
El camino recorrido
Empezamos viendo los tests como algo tedioso: lentos, frágiles y difíciles de mantener. Y terminamos con un arsenal completo:
- Continuous Testing que avisa al instante cuando algo se rompe
- Dev Services que levanta bases de datos reales sin configuración manual
- REST-assured que convierte los tests HTTP en código legible
- @InjectMock que elimina la dependencia de servicios externos
- @QuarkusIntegrationTest que valida el JAR y el binario nativo reales
En el Quarkiverso, los tests no son una carga: son tu red de seguridad. Y ahora sabes exactamente cómo tejerla.
La próxima vez que alguien diga “el test falló porque el servicio mock online estaba caído”, sonríe, toma un sorbo de café y muéstrale cómo se hace.
Código fuente: github.com/quarkiverso/testing-quarkus

