Testing Quarkus REST Services with @QuarkusTest and RestAssured
Quarkus has become one of the most compelling Java frameworks for building cloud-native microservices. Its supersonic startup time and low memory footprint make it ideal for containers and serverless deployments. But fast startup means nothing if your services aren't properly tested. This guide covers everything you need to know about testing Quarkus REST services using @QuarkusTest, RestAssured, and the broader Quarkus testing ecosystem.
Why Quarkus Testing Is Different
Quarkus takes an opinionated approach to testing that differs from traditional Spring Boot or Jakarta EE patterns. Rather than spinning up a full application server, Quarkus uses a Dev Mode testing model where the application boots once per test suite (not per test class), keeping test execution fast.
The Quarkus test extension integrates with JUnit 5 and provides specialized annotations that control how the application context is managed during tests.
Setting Up the Testing Dependencies
Add the following to your pom.xml:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>If you use Gradle, add to build.gradle:
testImplementation 'io.quarkus:quarkus-junit5'
testImplementation 'io.rest-assured:rest-assured'Writing Your First @QuarkusTest
The @QuarkusTest annotation is the foundation. It starts the Quarkus application before tests run and tears it down after. Here's a basic REST endpoint and its test:
// The endpoint
@Path("/greetings")
@Produces(MediaType.APPLICATION_JSON)
public class GreetingResource {
@GET
@Path("/{name}")
public Greeting greet(@PathParam("name") String name) {
return new Greeting("Hello, " + name + "!");
}
}// The test
@QuarkusTest
class GreetingResourceTest {
@Test
void testGreetEndpoint() {
given()
.pathParam("name", "World")
.when()
.get("/greetings/{name}")
.then()
.statusCode(200)
.body("message", equalTo("Hello, World!"));
}
}RestAssured's fluent DSL maps naturally onto HTTP — given() sets up the request, when() specifies the method and path, and then() asserts the response.
Testing POST and Request Bodies
Testing mutations requires constructing request bodies. RestAssured handles JSON serialization cleanly:
@QuarkusTest
class OrderResourceTest {
@Test
void testCreateOrder() {
OrderRequest request = new OrderRequest("product-123", 2);
given()
.contentType(ContentType.JSON)
.body(request)
.when()
.post("/orders")
.then()
.statusCode(201)
.body("orderId", notNullValue())
.body("status", equalTo("PENDING"));
}
@Test
void testCreateOrderValidationFails() {
given()
.contentType(ContentType.JSON)
.body("{\"quantity\": -1}")
.when()
.post("/orders")
.then()
.statusCode(400);
}
}Mocking Dependencies with @InjectMock
When testing a resource that depends on a service, you don't want to invoke real downstream calls. Quarkus provides @InjectMock to replace CDI beans with Mockito mocks:
@QuarkusTest
class PaymentResourceTest {
@InjectMock
PaymentService paymentService;
@Test
void testPaymentProcessed() {
when(paymentService.process(any()))
.thenReturn(new PaymentResult("TXN-001", "SUCCESS"));
given()
.contentType(ContentType.JSON)
.body(new PaymentRequest("100.00", "USD"))
.when()
.post("/payments")
.then()
.statusCode(200)
.body("transactionId", equalTo("TXN-001"));
}
@Test
void testPaymentFailed() {
when(paymentService.process(any()))
.thenThrow(new PaymentException("Insufficient funds"));
given()
.contentType(ContentType.JSON)
.body(new PaymentRequest("100.00", "USD"))
.when()
.post("/payments")
.then()
.statusCode(402);
}
}Using @TestHTTPEndpoint for Cleaner Tests
Quarkus provides @TestHTTPEndpoint to bind a test class to a specific resource, eliminating hardcoded paths:
@QuarkusTest
@TestHTTPEndpoint(UserResource.class)
class UserResourceTest {
@Test
void testGetUser() {
given()
.pathParam("id", "user-42")
.when()
.get("/{id}") // no /users prefix needed
.then()
.statusCode(200)
.body("id", equalTo("user-42"));
}
}This approach is more resilient to path changes — if UserResource moves from /users to /api/v1/users, your tests don't need updating.
Integration Testing with Testcontainers
For tests that need a real database, Quarkus integrates with Testcontainers through @QuarkusTestResource:
public class PostgresResource implements QuarkusTestResourceLifecycleManager {
private static final PostgreSQLContainer<?> POSTGRES =
new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Override
public Map<String, String> start() {
POSTGRES.start();
return Map.of(
"quarkus.datasource.jdbc.url", POSTGRES.getJdbcUrl(),
"quarkus.datasource.username", POSTGRES.getUsername(),
"quarkus.datasource.password", POSTGRES.getPassword()
);
}
@Override
public void stop() {
POSTGRES.stop();
}
}
@QuarkusTest
@QuarkusTestResource(PostgresResource.class)
class UserRepositoryTest {
@Inject
UserRepository userRepository;
@Test
@Transactional
void testSaveAndFindUser() {
User user = new User("alice@example.com", "Alice");
userRepository.persist(user);
User found = userRepository.findByEmail("alice@example.com");
assertThat(found).isNotNull();
assertThat(found.getName()).isEqualTo("Alice");
}
}Quarkus 3.x also supports @WithTestResource directly on fields for more granular control.
Testing Profiles with @QuarkusTestProfile
Different test scenarios often require different configurations. Quarkus test profiles let you customize the application context per test class:
public class MockServicesProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of(
"external.payment.url", "http://localhost:9090/mock",
"feature.new-checkout", "true"
);
}
@Override
public Set<Class<?>> getEnabledAlternatives() {
return Set.of(MockEmailService.class);
}
}
@QuarkusTest
@TestProfile(MockServicesProfile.class)
class CheckoutFeatureTest {
// Tests run with mock external services and feature flag enabled
}Native Image Testing with @QuarkusIntegrationTest
When targeting GraalVM native builds, you need a separate test layer that exercises the compiled binary:
@QuarkusIntegrationTest
class NativeGreetingResourceIT extends GreetingResourceTest {
// Inherits all tests from GreetingResourceTest
// but runs against the native binary
}Run native tests with:
./mvnw verify -PnativeTesting WebSocket Endpoints
Quarkus supports testing WebSocket endpoints with the @QuarkusTest annotation plus the WebSocket client:
@QuarkusTest
class ChatEndpointTest {
@Test
void testWebSocketMessage() throws Exception {
URI uri = URI.create("ws://localhost:8081/chat/room1");
try (Session session = ContainerProvider.getWebSocketContainer()
.connectToServer(TestClient.class, uri)) {
session.getBasicRemote().sendText("Hello!");
assertThat(TestClient.received)
.containsExactly("Echo: Hello!");
}
}
}Organizing Tests in Quarkus Projects
Structure your test directories to separate unit tests from integration tests:
src/
test/java/
com/example/
unit/ # Pure unit tests — no @QuarkusTest needed
OrderCalculatorTest.java
PriceFormatterTest.java
integration/ # @QuarkusTest — boots application
OrderResourceTest.java
PaymentResourceTest.java
native/ # @QuarkusIntegrationTest
OrderResourceIT.javaUnit tests in the unit/ package run without the Quarkus test framework overhead, keeping your fast feedback loop intact for business logic validation.
Continuous Testing with Quarkus Dev Mode
Quarkus's continuous testing mode runs tests automatically as you save files:
./mvnw quarkus:testIn dev mode, press r to run tests, o to toggle output, h for help. The test engine only re-runs tests affected by your changes, giving you near-instant feedback during development.
Connecting Quarkus Tests to Broader QA
Unit and integration tests catch code-level issues. But verifying that your Quarkus microservice actually behaves correctly end-to-end — across authentication, API contracts, and user workflows — requires a higher-level layer.
HelpMeTest lets you write plain-English tests that run against your deployed Quarkus services. You can set up health checks with helpmetest health quarkus-api 30s to monitor your service continuously, and write behavior-driven scenarios without Robot Framework expertise. It integrates directly with your CI pipeline and alerts you when services regress between deployments.
Key Takeaways
@QuarkusTestboots the full application once per test suite — keep tests independent but leverage the shared context@InjectMockreplaces CDI beans with Mockito mocks without restarting the application@TestHTTPEndpointbinds tests to resource classes, making them resilient to path changes- Testcontainers with
@QuarkusTestResourceprovides real database testing without environment coupling @QuarkusIntegrationTestvalidates native image behavior — critical for GraalVM deployments- Continuous testing mode in Dev Mode eliminates the compile-test cycle friction
Quarkus's testing infrastructure is one of its strongest selling points. The fast boot, profile system, and first-class RestAssured integration make it possible to maintain comprehensive test coverage without sacrificing developer velocity.