Mastering Quarkus Testing: When to Use @QuarkusTest vs. @QuarkusIntegrationTest
Understand test isolation like a pro, avoid hidden classpath bugs, and write faster, more reliable tests for modern Java applications.
Testing is where the magic happens in Quarkus. It is the safety net that lets you build fast, optimize early, and ship with confidence. But when you're staring at two seemingly similar annotations: @QuarkusTest
and @QuarkusIntegrationTest
, you might wonder which one to choose. This hands-on guide will not only answer that question but also take you under the hood of how Quarkus handles test execution and isolation.
Whether you're writing business logic tests, mocking beans, or validating native executables, this tutorial will walk you through practical examples, quirks, and insights.
The Mission
By the end of this tutorial, you’ll be able to:
Understand how
@QuarkusTest
and@QuarkusIntegrationTest
execute tests differently.Choose the right one based on test scope and goals.
Learn how classloaders in
@QuarkusTest
isolate your tests and prevent library chaos.Recognize when you need full process isolation instead of just classloader separation.
Apply both annotations to real-world Quarkus testing scenarios.
@QuarkusTest — Testing Inside the Box
Think of @QuarkusTest
as a fast-moving integration tool. It launches your application inside the same JVM as your JUnit test runner, but cleverly uses separate classloaders to keep the application code and test code from stepping on each other.
How It Works
Quarkus starts a lightweight application context inside your test process. Your application beans can be injected directly using @Inject
. Even though everything runs in the same JVM, classloader separation creates two distinct environments:
The Application ClassLoader loads your production code (
src/main/java
) and its declared dependencies.The Test ClassLoader handles test classes (
src/test/java
) and their specific dependencies.
This means your test-only libraries (like mocking frameworks or newer versions of shared libraries) stay in their lane. You can test how your beans behave without worrying about test tools interfering with your actual app.
Why This Matters
Imagine your application uses version 1.8.0 of commons-beanutils
, but your test helper library pulls in version 1.9.4. If all code ran on the same classpath, you'd likely hit version conflicts and runtime errors. But with separate classloaders, your application and test utilities can each use their preferred version without collision.
This isolation gives you confidence that what passes in tests will behave the same when deployed.
Example: Component-Level Testing
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@QuarkusTest
public class MyServiceTest {
@Inject
MyApplicationService service;
TestHelper helper = new TestHelper();
@Test
void testServiceLogic() {
String input = helper.generateTestData();
String expected = "Processed: " + input;
String actual = service.process(input);
assertEquals(expected, actual);
}
}
In this test:
MyApplicationService
is loaded with application dependencies.TestHelper
might depend on different versions of libraries, but is safely isolated.Both can coexist peacefully inside the same JVM.
When to Use @QuarkusTest
You need direct bean access using
@Inject
.You're mocking services or repositories using
@InjectMock
.You're testing business logic or JAX-RS resources with mocked persistence layers.
You want fast feedback during development.
@QuarkusIntegrationTest — Testing Outside the Box
Now let’s flip the switch. @QuarkusIntegrationTest
is your go-to when testing the real, deployable version of your app; whether it is a runnable JAR, native executable, or container image.
How It Works
Quarkus first packages your app. Then it launches that packaged artifact as a separate process. Your test runs in its own JVM and talks to the app via external channels, usually HTTP.
This is true black-box testing. There is no bean injection, no peeking under the hood, just interacting with the app like a real client.
Isolation by Process
The separation here is enforced by the operating system. Your test and application run in different memory spaces, use different classpaths, and never share classloaders. This prevents even the possibility of accidental interference.
Example: REST API End-to-End Test
import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.hasSize;
@QuarkusIntegrationTest
public class ItemResourceIT {
@Test
void testItemsEndpointFlow() {
RestAssured.given()
.contentType(ContentType.JSON)
.body("{\"name\":\"My Test Item\"}")
.post("/items")
.then()
.statusCode(201);
RestAssured.get("/items")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("$", hasSize(1))
.body("[0].name", is("My Test Item"));
}
}
This test:
Starts the application in a real, packaged form.
Sends requests over HTTP.
Validates that the application behaves correctly end-to-end.
When to Use @QuarkusIntegrationTest
You’re validating native builds or container images.
You need to verify production-like behavior of REST APIs.
You’re testing configuration as bundled in the final artifact.
You want to simulate real client behavior interacting over HTTP.
Comparison: Which Should You Use?
Here’s a quick breakdown in plain terms:
@QuarkusTest
runs tests in the same JVM as your app but uses separate classloaders. It’s fast and great for component-level testing.@QuarkusIntegrationTest
runs your app in a separate process and tests it externally. It’s slower but necessary for verifying the deployable artifact.
Let’s talk characteristics.
When using @QuarkusTest
, the application is executed in-process. The test targets the source code and classpath directly. Isolation is handled via classloaders, which ensures faster startup, easier mocking with bean injection, and reliable component integration testing. However, it does not test the packaging or external-facing behavior of the final artifact.
With @QuarkusIntegrationTest
, the application runs out-of-process. It targets the packaged artifact — whether JAR, native binary, or container image. Isolation is achieved through process boundaries. This provides the highest level of realism for testing, though at the cost of slower startup and reduced ability to mock internal beans. It shines in end-to-end, black-box scenarios.
Final Thoughts: Testing Strategy
The answer to “Which one should I use?” is both.
Use
@QuarkusTest
liberally during development to validate bean wiring, business logic, and internal APIs.Use
@QuarkusIntegrationTest
sparingly but critically to validate that your packaged application behaves correctly in a production-like environment.
Think of this as your testing pyramid:
Lots of fast unit tests (plain JUnit)
A solid set of
@QuarkusTest
component testsA few essential
@QuarkusIntegrationTest
full-stack validations
Together, they create a resilient, maintainable, and production-ready Quarkus application.
Bonus Tips
Use
application-test.properties
to customize test configurations.Quarkus Dev Services can spin up external dependencies like databases or Kafka without writing extra code.
Run
mvn quarkus:dev
and pressr
for blazing-fast continuous testing.
Further Learning from quarkus.io
Want to go deeper? The official Quarkus documentation offers excellent resources to expand your testing skills and cover advanced use cases:
Testing your application
A solid starting point for understanding Quarkus testing basics, including setup, annotations, and best practices.Dev Services
Learn how Quarkus can automatically spin up databases, Kafka brokers, and other services during tests, with zero config.Continuous Testing
Discover how to get near-instant feedback using Quarkus’s live testing features right in dev mode.
These resources will round out your testing toolbox and help you write efficient, production-grade test suites tailored for modern Quarkus applications.
Recap
@QuarkusTest
: Great for internal testing with bean access and classloader isolation.@QuarkusIntegrationTest
: Best for full-stack, production-like validation through process isolation.
Master both. Your test suite will thank you.