This is a cache of https://developer.ibm.com/tutorials/quarkus-basics-05/. It is a snapshot of the page as it appeared on 2025-12-05T02:53:43.998+0000.
Testing Quarkus applications - IBM Developer

Tutorial

Testing Quarkus applications

Discover the best practices for writing unit tests, integration tests, and production-like tests in Quarkus

In this tutorial, you will go beyond the basics and explore the different testing features that Quarkus provides. You have already seen simple REST tests, so now the focus is on understanding the different types of tests Quarkus supports and when to use them.

You will learn the difference between plain JUnit unit tests, Quarkus integration tests with @QuarkusTest, and production-like tests with @QuarkusIntegrationTest. You will also see how Quarkus enables continuous testing in dev mode, how to test error handling, and how to replace dependencies with mocks during tests.

Prerequisites

Before you begin, make sure that you have a Quarkus project from an earlier tutorial (such as the "Building REST APIs with Quarkus" or "Mastering data persistence with Quarkus" tutorial) that you can use as a base. You will add tests to this existing project to explore the different testing approaches in Quarkus.

Testing layers in Quarkus

Testing in Quarkus builds on top of JUnit 5, which many Java developers already know. What Quarkus adds is an easy way to test your application in different modes, from isolated unit tests to full integration tests with Dev Services. Knowing which test type to use saves time and keeps your test suite efficient. Think of it as three layers:

  • @Test (plain JUnit). These are regular unit tests that don’t start Quarkus. They are fast and used for testing pure Java logic (for example, helper classes or algorithms).
  • @Quarkusteststarts. Quarkus in dev/test mode before you run your tests, as real runtime tests. This lets you test REST endpoints, database access, and CDI injection inside a real Quarkus runtime. tests run in the same JVM as your app.
  • @QuarkusIntegrationTestRuns. Run your tests against the packaged application (JAR or native image). This simulates how your app will behave in production. These tests take longer, but they give you confidence that your deployment will work.

Step 1. Write a plain unit test

Not all code depends on Quarkus. Many parts of your application are just plain Java logic, like string operations, math calculations, or utility classes. You can test these pieces with JUnit directly. These tests are very fast because they don’t start any extra framework; only the JVM runs them.

When we say Quarkus isn’t “running,” we mean that:

  • No REST endpoints are started.
  • No CDI (dependency injection) context is available.
  • No database is started.
  • Dev Services is not started.
  • Only your test class and the code it calls are loaded by the JVM.

This is the simplest and fastest type of test. It’s great for checking business rules or helper methods without waiting for Quarkus to boot.

For example, this test runs instantly and does not start Quarkus:

package com.ibm.developer;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class GreetingServiceTest {

    @Test
    public void testUppercaseGreeting() {
        GreetingService service = new GreetingService();
        String result = service.toUpperCase("hello");
        assertEquals("HELLO", result);
    }
}

Step 2. Run tests inside Quarkus with @QuarkusTest

Sometimes you want to test more than plain Java logic. You want to check that your REST endpoints respond correctly, that CDI injections work, or that your database queries return real results. For this, you need Quarkus itself to start.

When you use @QuarkusTest, Quarkus boots up in test mode before running your tests. This is not the full production startup, but a lightweight mode that still gives you almost everything:

  • The Quarkus runtime starts, similar to dev mode.
  • CDI (Contexts and Dependency Injection) is active, so you can inject beans and services.
  • REST endpoints are deployed and can be called by your tests.
  • Dev Services can start automatically, for example PostgreSQL or Kafka, so you test against real services without manual setup.

The key difference from a plain JUnit test is that now you are testing your application inside a running Quarkus environment.

In the following example, Quarkus starts your application, exposes the /greeting endpoint, and your test verifies the result by using RESTAssured.

package com.ibm.developer;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
public class GreetingResourceTest {

    @Test
    public void testGreetingEndpoint() {
        given()
          .when().get("/greeting")
          .then()
             .statusCode(200)
             .body("message", is("Hello from configuration!"));
    }
}

Behind the scenes, this is what's happening:

  • Quarkus starts once for the test class.
  • Your resource and configuration are loaded.
  • The test sends a real HTTP call to /greeting.
  • Quarkus handles the request and returns a JSON response, just like in real life.

@QuarkusTest is perfect for integration tests that verify how your application behaves when its components work together.

Step 3. Run a production-like test with @QuarkusIntegrationTest

Unit tests and @QuarkusTest give you fast feedback, but they don’t run your application exactly like it will run in production. Sometimes problems only appear after packaging, such as missing resources in the JAR, configuration issues, or native image differences. That’s why Quarkus also supports integration tests that run against the packaged application.

When you annotate a test with @QuarkusIntegrationTest, Quarkus does something special:

  • First, Quarkus builds your application as a JAR or a native binary.
  • Then, Quarkus launches that packaged application as an external process.
  • Finally, the tests connect to that running process by using real HTTP requests.

This means your tests no longer run “inside” Quarkus like they do with @QuarkusTest. Instead, they talk to the application from the outside. Just like a client or user would.

In the following example, GreetingResourceIT extends GreetingResourceTest. This means that the same endpoint tests are run again, but this time against the packaged application.

package com.ibm.developer;

import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest
public class GreetingResourceIT extends GreetingResourceTest {
    // Runs the same tests as GreetingResourceTest,
    // but against the packaged application.
}

Behind the scenes, this is what's happening:

  • Quarkus packages your app (./mvnw package -Dnative if you build a native binary, otherwise a JAR).
  • The packaged app is started as if it were running in production.
  • Your test class connects over HTTP to that running process.
  • Once all tests are done, the process is stopped.

This is slower than @QuarkusTest, because building and starting the application takes time. But it gives you high confidence that your deployment will work the same way in production.

Use this test:

  • Before you release your app to production.
  • When you are testing native builds.
  • For end-to-end tests or smoke tests that confirm that the packaged app works.

Step 4. Test error handling

Applications don’t only need to succeed. Sometimes we also want them to fail in the right way. Testing error responses ensures that your users see correct status codes and messages.

The following example confirms that trying to delete a non-existing resource gives the expected error.

@Test
public void testDeleteUnknownFruit() {
    given()
      .when().delete("/fruits/9999")
      .then()
         .statusCode(404); // Not Found
}

Step 5. Use continuous testing in dev mode

You have already seen continuous testing mode in earlier tutorials in this series, so this is a quick recap. Normally, developers write code, save it, and then run tests manually. This means that you only see failures after you decide to check. With Quarkus, tests run continuously in dev mode. Every time that you change code or configuration, Quarkus reruns the tests automatically. This gives you instant feedback and helps you fix problems before they grow.

When you run your app in dev mode (quarkus dev), Quarkus:

  • Watches your project files for changes (Java code, resources, configuration).
  • Quickly recompiles only what changed.
  • Figures out which tests are affected by those changes. For example:

    • If you edit a REST resource, only the tests that hit that endpoint need to run.
    • If you edit a utility class used by many components, more tests might need to rerun.
  • Runs those tests automatically and shows results in the terminal.

This smart tracking is why continuous testing feels much faster than rerunning the entire test suite every time.

Try it out! Start dev mode:

quarkus dev

You can also control test behavior in the dev mode console:

  • Press r to rerun all tests.
  • Press o to toggle test output.
  • Press p to pause continuous testing.

Continuous testing in dev mode saves time, reduces errors, and makes testing feel like a natural part of coding, not an extra chore.

Step 6. Try mocking, an advanced testing techniques

In real projects, your code often depends on external systems, like remote REST APIs, message brokers, or third-party services. When testing, you usually don’t want to call those real systems. They might be slow, unavailable or cost money to use. Instead, you use mocks, which are lightweight replacements that simulate the behavior of the real service.

Quarkus makes mocking easy through CDI alternatives. You can write a fake version of a service and tell Quarkus to use it only during tests. This way, your tests stay fast and reliable.

Let’s say you have a service that fetches a greeting from a remote API:

package com.ibm.developer;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class GreetingService {
    public String getGreeting() {
        // Imagine this calls a remote API
        return "Hello from remote service!";
    }
}

Your REST resource uses this service:

@Path("/hello")
public class HelloResource {

    @Inject
    GreetingService service;

    @GET
    public String hello() {
        return service.getGreeting();
    }
}

Now in your tests, you don’t want to call the real remote API. You can provide a mock instead:

package com.ibm.developer;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.annotation.Priority;

@Alternative
@Priority(1) // Higher priority than the real bean
@ApplicationScoped
public class MockGreetingService extends GreetingService {

    @Override
    public String getGreeting() {
        return "Hello from mock!";
    }
}

When you run your tests with @QuarkusTest, Quarkus will use this mock instead of the real GreetingService.

@QuarkusTest
public class HelloResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("Hello from mock!"));
    }
}

The test passes because the mock is used.

Some other mocking options include:

  • WireMock: Simulate HTTP services with predefined responses.
  • Dev Services: Automatically start lightweight versions of Kafka, databases, or other infrastructure.
  • Mockito: Still possible if you prefer classic mocking libraries.

Mocking lets you test your code in isolation. You control dependencies, avoid flaky tests, and keep your test suite fast. This is especially important in enterprise applications where external systems may not always be available.

Summary

You learned how Quarkus provides three distinct testing layers each serving different purposes in your testing strategy: plain JUnit unit tests for fast isolated logic, @QuarkusTest for integration tests within a running Quarkus environment, and @QuarkusIntegrationTest for production-like tests against packaged applications. You also discovered how to use CDI alternatives for mocking dependencies and how continuous testing provides immediate feedback during development, helping you choose the right test approach for different scenarios.