Testing Quarkus Applications
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Quarkus tests sit on four levels that trade isolation for realism: plain JUnit 5 unit tests with no Quarkus
involvement at all, @QuarkusComponentTest for a narrow slice of the CDI bean graph, @QuarkusTest for the full
application context running in the same JVM as the test, and @QuarkusIntegrationTest for the packaged artifact
running as a separate black-box process. This page covers all four, plus the mocking mechanisms, test resources,
and tooling (continuous testing, JaCoCo coverage, @TestSecurity) that go with them.
./mvnw quarkus:add-extension -Dextensions="quarkus-junit5,rest-assured"
quarkus-junit5 supplies the @QuarkusTest/@QuarkusIntegrationTest JUnit 5 extensions; rest-assured (a
plain test-scoped dependency, not a runtime extension) is the de facto standard for exercising HTTP endpoints
from a test and is pre-wired to the running test port with no extra configuration.
@QuarkusTest and the test lifecycle
io.quarkus.test.junit.QuarkusTest boots the full Quarkus application — CDI container, HTTP layer, configured
datasources, everything — once, in the same JVM the test runs in, and injects into the test class exactly as it
would into any other bean:
package com.example.book;
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.inject.Inject;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
@QuarkusTest
class BookServiceTest {
@Inject
BookService bookService;
@Test
void createsABook() {
Book book = bookService.create("1984", "George Orwell");
assertThat(book.id).isNotNull();
}
}
Quarkus starts the application once per distinct configuration and reuses it across every @QuarkusTest class
that shares that same configuration, rather than restarting between test classes — this is what keeps a large
@QuarkusTest suite fast despite each test running against a real, fully wired application. The application
restarts only when the next test class requests a different configuration, most commonly through @TestProfile
(covered below); ordering test classes by profile, or accepting the framework’s own ordering, minimizes how many
restarts a full suite pays for.
Testing HTTP endpoints with RestAssured
@QuarkusTest starts the HTTP layer on a real (random, by default) port and RestAssured’s given()/when()/then()
already targets it with no base-URI configuration needed:
package com.example.book;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
@QuarkusTest
class BookResourceTest {
@Test
void createsAndReadsABookThroughHttp() {
String location =
given()
.contentType("application/json")
.body("""
{"title": "1984", "author": "George Orwell"}
""")
.when().post("/books")
.then().statusCode(201)
.extract().header("Location");
given()
.when().get(location)
.then().statusCode(200)
.body("title", is("1984"));
}
}
This exercises the resource class documented on the REST services page end to end — serialization, validation, and the actual routed method — rather than calling the Java method directly, which is the point: it is the only one of the annotations on this page that verifies the HTTP contract itself, not just the code behind it.
Injection in tests and @TestProfile
Any CDI injection point valid in application code — @Inject, a qualified @RestClient, a Panache
repository — works identically inside a @QuarkusTest class, since the test class itself becomes part of the
same CDI container. io.quarkus.test.junit.QuarkusTestProfile lets a test class run the application under
different configuration, a different active profile, or a different set of enabled beans than the default test
run, without touching application.properties:
package com.example.book;
import java.util.Map;
import java.util.Set;
import io.quarkus.test.junit.QuarkusTestProfile;
public class InMemoryCatalogTestProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of("catalog.cache.enabled", "false");
}
@Override
public Set<Class<?>> getEnabledAlternatives() {
return Set.of(InMemoryBookRepository.class);
}
@Override
public String getConfigProfile() {
return "catalog-test";
}
}
package com.example.book;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import org.junit.jupiter.api.Test;
@QuarkusTest
@TestProfile(InMemoryCatalogTestProfile.class)
class BookServiceWithInMemoryCatalogTest {
@Test
void worksAgainstTheInMemoryRepositoryAlternative() {
// exercises BookService against InMemoryBookRepository instead of the real one
}
}
getConfigOverrides() supplies extra configuration keys (read the same way a @ConfigMapping-bound
property would be); getEnabledAlternatives() selectively activates one or more @Alternative-annotated beans
for just this profile’s tests, without a global quarkus.arc.selected-alternatives setting affecting every other
test; getConfigProfile() names the active Quarkus profile the way %catalog-test. property prefixes or
profile-specific YAML/properties files would be picked up. Because this is a different configuration from the
default test run, Quarkus restarts the application for test classes carrying this @TestProfile — and again
when a later test class reverts to the default profile — which is the concrete cost @TestProfile trades for
the ability to test alternate wiring.
Mocking
@InjectMock and @InjectSpy
io.quarkus.test.junit.mockito.InjectMock swaps an injected CDI bean for a Mockito mock for the duration of a
test class; @InjectSpy does the same but wraps the real bean instead of replacing it, so unstubbed calls fall
through to the actual implementation while stubbed/verified calls behave like an ordinary Mockito spy:
package com.example.book;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import jakarta.inject.Inject;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import io.quarkus.test.junit.mockito.InjectSpy;
import org.junit.jupiter.api.Test;
@QuarkusTest
class BookServiceMockingTest {
@InjectMock
BookRepository repository;
@InjectSpy
NotificationService notificationService;
@Inject
BookService bookService;
@Test
void publishesANotificationOnCreate() {
Book book = new Book();
book.title = "1984";
when(repository.findById(1L)).thenReturn(book);
bookService.create("1984", "George Orwell");
verify(notificationService).notifyCreated(book);
}
}
QuarkusMock
io.quarkus.test.junit.QuarkusMock installs a mock programmatically, from inside a test method rather than as
a @InjectMock field fixed for the whole class — useful when different test methods in the same class need a
different mock (or the real bean) for the same injection point:
package com.example.book;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import jakarta.inject.Inject;
import io.quarkus.test.junit.QuarkusMock;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
@QuarkusTest
class BookServiceQuarkusMockTest {
@Inject
BookRepository repository;
@Inject
BookService bookService;
@Test
void usesAPerTestMock() {
BookRepository mockRepository = mock(BookRepository.class);
when(mockRepository.count()).thenReturn(1L);
QuarkusMock.installMockForType(mockRepository, BookRepository.class);
// bookService now sees mockRepository through its existing injection point
}
}
PanacheMock
Mocking an active-record Panache entity’s static methods (Book.listAll(), Book.findById(id)) needs
io.quarkus.panache.mock.PanacheMock instead of @InjectMock, since Mockito cannot stub a static call the same
way it stubs a call on an injected bean — this is covered with a full example on
the Panache page’s testing section, alongside why the
repository style needs no Panache-specific mocking API at all.
Mocking a REST client
A typed @RestClient interface is just another CDI bean, so @InjectMock
replaces it exactly like any other collaborator when a unit test only needs to stub its return value:
package com.example.client;
import static org.mockito.Mockito.when;
import jakarta.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import org.junit.jupiter.api.Test;
@QuarkusTest
class OrderServiceRestClientMockTest {
@InjectMock
@RestClient
OrdersApi ordersApi;
@Inject
OrderService orderService;
@Test
void usesTheMockedClient() {
when(ordersApi.findById("o-1")).thenReturn(new Order("o-1", 5));
// orderService now calls the mocked OrdersApi instead of a real HTTP endpoint
}
}
Testing the client’s actual HTTP behavior — headers, path building, error mapping — needs a real HTTP server to
point it at instead: a WireMock instance started for the test via @QuarkusTestResource, with the client’s url
redirected to it:
package com.example.client;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
public class WireMockOrdersApiResource implements QuarkusTestResourceLifecycleManager {
private WireMockServer wireMockServer;
@Override
public Map<String, String> start() {
wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
wireMockServer.start();
wireMockServer.stubFor(get(urlEqualTo("/orders/o-1"))
.willReturn(aResponse().withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("""
{"id": "o-1", "quantity": 5}
""")));
return Map.of("quarkus.rest-client.orders-api.url", wireMockServer.baseUrl());
}
@Override
public void stop() {
if (wireMockServer != null) {
wireMockServer.stop();
}
}
}
package com.example.client;
import jakarta.inject.Inject;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@QuarkusTest
@QuarkusTestResource(WireMockOrdersApiResource.class)
class OrdersApiWireMockTest {
@Inject
OrderService orderService;
@Test
void readsTheOrderFromTheStub() {
assertThat(orderService.findQuantity("o-1")).isEqualTo(5);
}
}
@QuarkusTestResource and QuarkusTestResourceLifecycleManager
The WireMockOrdersApiResource above is an ordinary
io.quarkus.test.common.QuarkusTestResourceLifecycleManager: start() runs before the application boots and
returns a map of configuration properties injected into the test’s configuration (exactly how the WireMock
server’s dynamic port reaches quarkus.rest-client.orders-api.url above without either side hardcoding it), and
stop() tears the resource down after the test class finishes. @QuarkusTestResource on a test class wires one
in.
This is the same mechanism Dev
Services itself is built on — Dev Services already auto-starts and auto-configures a Testcontainers-backed
Postgres, Kafka, or Keycloak container for a test with no @QuarkusTestResource at all, as covered on
the datasources and transactions page and
the JWT/OIDC/Keycloak page. Reaching for a hand-written
QuarkusTestResourceLifecycleManager instead only makes sense for a container or fixture Dev Services has no
built-in support for (WireMock above is one example), or one that needs configuration or a lifecycle Dev
Services' zero-config defaults do not expose:
package com.example.book;
import java.util.Map;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
public class RedisTestResource implements QuarkusTestResourceLifecycleManager {
private GenericContainer<?> redis;
@Override
public Map<String, String> start() {
redis = new GenericContainer<>(DockerImageName.parse("redis:7"))
.withExposedPorts(6379);
redis.start();
return Map.of("quarkus.redis.hosts", "redis://" + redis.getHost() + ":" + redis.getMappedPort(6379));
}
@Override
public void stop() {
if (redis != null) {
redis.stop();
}
}
}
Component testing with @QuarkusComponentTest
./mvnw quarkus:add-extension -Dextensions=quarkus-junit5-component
io.quarkus.test.component.QuarkusComponentTest builds only the bean graph a test actually needs — the CDI
beans reachable from the beans under test, with anything else stubbed or absent — instead of booting the entire
application, which makes it start markedly faster than @QuarkusTest for a service with a handful of
collaborators and no need for the HTTP layer, a datasource, or Dev Services at all:
package com.example.book;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import io.quarkus.test.component.TestConfigProperty;
import io.quarkus.test.junit.mockito.InjectMock;
import org.junit.jupiter.api.Test;
@QuarkusComponentTest
@TestConfigProperty(key = "catalog.default-author", value = "Unknown")
class BookServiceComponentTest {
@InjectMock
BookRepository repository;
@Inject
BookService bookService;
@Test
void fallsBackToTheDefaultAuthor() {
Book book = bookService.create("1984", null);
assertThat(book.author).isEqualTo("Unknown");
}
}
This trades away everything @QuarkusTest provides beyond CDI itself (no real HTTP layer, no Dev Services, no
@ConfigMapping-bound configuration sources beyond what @TestConfigProperty supplies) for startup time close
to a plain unit test — the right choice for a service class with several collaborators that does not itself need
end-to-end HTTP or database coverage, leaving that coverage to a smaller number of `@QuarkusTest`s.
@QuarkusIntegrationTest
package com.example.book;
import static io.restassured.RestAssured.given;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import org.junit.jupiter.api.Test;
@QuarkusIntegrationTest
class BookResourceIT {
@Test
void createsABookAgainstThePackagedArtifact() {
given()
.contentType("application/json")
.body("""
{"title": "1984", "author": "George Orwell"}
""")
.when().post("/books")
.then().statusCode(201);
}
}
io.quarkus.test.junit.QuarkusIntegrationTest does not run the application in the test’s own JVM at all: it
packages the application (respecting whatever build the active Maven/Gradle invocation already produced — a
runnable JAR, a native executable, or a container image) and launches it as a separate external process, then
drives it purely over the network. This changes what a test in this class can do, compared to @QuarkusTest:
-
No CDI injection. An
@Injectfield in a@QuarkusIntegrationTestclass staysnull— the application under test is a different OS process with its own JVM (or none at all, for native), so there is no shared CDI container to inject from. Every assertion has to go through the application’s actual external interface: HTTP, a message broker, whatever it exposes. -
Black-box only.
@InjectMock/@InjectSpy/QuarkusMock/PanacheMockdo not apply, for the same reason — there is no in-process bean to swap. A@QuarkusTestResourcestill works, since it only supplies configuration/environment for the external process to pick up at startup, not anything injected into it. -
By convention these classes are named
*IT(Failsafe’s integration-test naming) rather than*Test, so they run in Maven’sverifyphase, after packaging, rather than intestalongside@QuarkusTest.
This is deliberately the closest a test gets to what actually ships — see the architecture and build-time page for what packaging and native compilation change under the hood — which is exactly why it belongs at the top of the pyramid below: few, slow, but the only level that would catch a bug specific to the packaged artifact itself (a resource missing from a native reflection configuration, a container image with the wrong entrypoint) rather than to application code.
Native testing
Pointing @QuarkusIntegrationTest at a native executable instead of a JAR needs that executable already built
(./mvnw package -Pnative, or -Dquarkus.native.container-build=true to build it in a container without a local
GraalVM/Mandrel install) — the test class itself does not change. Native tests exist specifically to catch what
JVM-mode tests structurally cannot: a reflective call GraalVM’s closed-world static analysis did not see and
therefore stripped, a resource not bundled into the image, or a native-image build failure itself. They are
correspondingly the slowest and least frequently run level of the pyramid, typically reserved for a dedicated CI
stage rather than every local build.
Continuous testing
With quarkus dev running, pressing r re-runs only the tests affected by the last save, in the same terminal,
without switching to the IDE’s test runner — the primary way Quarkus applications get tested during day-to-day
development. This is covered in full, including the full-suite (f) and break-on-failure (b) keys, on
the developer experience page; every
annotation on this page (@QuarkusTest, @QuarkusComponentTest, @QuarkusIntegrationTest) participates in
continuous testing the same way.
Test coverage with JaCoCo
./mvnw quarkus:add-extension -Dextensions=jacoco
quarkus.jacoco.report=true
quarkus.jacoco.report-location=jacoco-report
With quarkus-jacoco on the classpath, ./mvnw test (or ./mvnw verify) instruments application classes and
produces a JaCoCo report under target/jacoco-report once the suite finishes, covering exercised code across
every test level that actually runs inside the JVM — plain unit tests, @QuarkusComponentTest, and
@QuarkusTest all contribute to the same coverage data.
@QuarkusIntegrationTest against a native executable is the one level JaCoCo structurally cannot measure:
coverage instrumentation works at the JVM bytecode level, and a native binary has already been ahead-of-time
compiled with no bytecode left to instrument at runtime (see
the architecture and build-time page for why). A native
@QuarkusIntegrationTest run still verifies behavior; it simply contributes nothing to the coverage report,
which should stay based on the JVM-mode @QuarkusTest/unit-test run.
Testing secured endpoints with @TestSecurity
io.quarkus.test.security.TestSecurity replaces the real authentication mechanism with a fixed, fake
SecurityIdentity for a test method — including the OIDC-claims variant via quarkus-test-security-oidc — so
@RolesAllowed/@PermissionsAllowed authorization logic can be exercised without a real identity provider or
even Dev Services for Keycloak running. It is covered in full, with both plain-role and OIDC-claims examples, on
the security overview and architecture
page's security-testing section.
Choosing the right level: the test pyramid
Plain JUnit 5/Mockito unit tests — covered in depth, independent of any framework, on
the Java reference — stay the base of the pyramid in a Quarkus
codebase exactly as anywhere else: fastest, most numerous, and the right default for a class with no CDI
collaborators worth wiring up. @QuarkusComponentTest and @QuarkusTest are Quarkus-specific middle layers with
no equivalent in a plain JUnit/Mockito suite; @QuarkusIntegrationTest occupies the same top-of-pyramid slot a
Spring Boot codebase fills with a black-box test against a packaged artifact. See
the Spring Boot unit and integration testing page for
that comparison in detail — Spring’s @WebMvcTest/@DataJpaTest slice tests and @SpringBootTest map roughly
onto the same fast-slice-vs-full-context trade-off as @QuarkusComponentTest vs. @QuarkusTest, though the
mechanisms are not interchangeable between the two frameworks.