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 @Inject field in a @QuarkusIntegrationTest class stays null — 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/PanacheMock do not apply, for the same reason — there is no in-process bean to swap. A @QuarkusTestResource still 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’s verify phase, after packaging, rather than in test alongside @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

flowchart TB subgraph Pyramid["Quarkus test pyramid (bottom = most tests, fastest)"] direction TB IT["Fewest: @QuarkusIntegrationTest\npackaged JAR / native executable / container\nblack-box, no injection, slowest"] APP["@QuarkusTest\nfull CDI context, real HTTP layer,\nRestAssured, Dev Services, @TestSecurity"] COMPONENT["@QuarkusComponentTest\nonly the needed CDI bean graph,\nno HTTP layer, no Dev Services, fast"] UNIT["Most: plain JUnit 5 + Mockito\nno Quarkus context at all, fastest"] end UNIT --> COMPONENT --> APP --> IT

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.