A practical guide for testing Quarkus applications

In this article we will learn how to create simple and advanced tests for your Quarkus applications using the REST Assured API.

Quarkus applications can be tested with any Java based testing framework. As most Quarkus applications are built upon a RESTful layer, the framework REST Assured has become a standard for Quarkus appliications.

REST Assured is a Java library that can be used to write powerful tests for REST APIs using a flexible Domain Specific Language (DSL). The fluent API available in REST Assured supports the standard patterns from Behavior-Driven Development (BDD) with its Given/When/Then syntax. The resulting test is simple to read and can include all the steps needed to build the test with just a one line of code.

Creating a basic Quarkus test

We will be testing the following CRUD REST Endpoint:

@Path("/customer")
@Produces("application/json")
@Consumes("application/json")
public class CustomerResource {

    @Inject
    EntityManager entityManager;

    @GET
    public Customer[] get() {
        return entityManager.createNamedQuery("Customers.findAll", Customer.class)
                .getResultList().toArray(new Customer[0]);
    }
    @POST
    @Transactional
    public Response create(Customer customer) {
        if (customer.getId() != null) {
            throw new WebApplicationException("Id was invalidly set on request.", 422);
        }

        entityManager.persist(customer);
        System.out.println("Created "+customer);
        return Response.ok(customer).status(201).build();
    }

    @PUT
    @Transactional
    public Customer update(Customer customer) {
        if (customer.getId() == null) {
            throw new WebApplicationException("Customer Id was not set on request.", 422);
        }

        Customer entity = entityManager.find(Customer.class, customer.getId());

        if (entity == null) {
            throw new WebApplicationException("Customer with id of " + customer.getId() + " does not exist.", 404);
        }

        entity.setName(customer.getName());

        return entity;
    }

    @DELETE
    @Transactional
    public Response delete(Customer customer) {
        Customer entity = entityManager.find(Customer.class, customer.getId());
        if (entity == null) {
            throw new WebApplicationException("Customer with id of " + customer.getId() + " does not exist.", 404);
        }
        entityManager.remove(entity);
        return Response.status(204).build();
    }
}

The most basic test of this application is to check if the “/customer” Endpoint is available:

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

@QuarkusTest
public class CustomerEndpointTest {

    @Test
    public void testCustomerService() {

        RestAssured.given()
                .when().get("/customer")
                .then()
                .statusCode(200);
    }
}

The @QuarkusTest annotation is built upon junit5, therefore to use it in combination with RestAssured fluent API, you need to following dependency in your application:

<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>

As far as configuration is concerned, the most common test settings you can plug in your configuration are:

quarkus.http.test-port=8083
quarkus.http.test-timeout=10s

Building an Advanced Quarkus Test class

Now let’s build a more complex test which checks the content of the Body returned by the REST service.

Within our example application (https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/testing) we have added an import.sql file with some default data:

INSERT INTO Customer(id, name) VALUES (1, 'Batman');
INSERT INTO Customer(id, name) VALUES (2, 'Superman');
INSERT INTO Customer(id, name) VALUES (3, 'Wonder woman');

Now let’s build a test for that:

@QuarkusTest
@TestHTTPEndpoint(CustomerResource.class)
public class CustomerEndpointAdvancedTest {

    @Test
    public void testCustomerService() {

        RestAssured.given()
                .when().get()
                .then()
                .statusCode(200)
                .body("$.size()", is(3),
                        "[0].id", is(1),
                        "[0].name", is("Batman"),
                        "[1].id", is(2),
                        "[1].name", is("Superman"),
                        "[2].id", is(3),
                        "[2].name", is("Wonder woman")
                );


        given()
                .contentType("application/json")
                .body(new Customer("Iron man"))
                .when()
                .post()
                .then()
                .statusCode(201);

         RestAssured.given()
                .when().get()
                .then()
                .statusCode(200)
                .body("$.size()", is(4),
                        "[3].name", is("Iron man")
                );

    }
}

First of all, notice we have now used the @TestHTTPEndpoint(CustomerResource.class). This allows us to use the default Path for the CustomerResource. This way we have decoupled the Endpoint from its Path so if we change the Path of the Customer resource we won’t have to change our Testing classes too.

Next, pay attention on how we have verified the content of the Response Body:

        RestAssured.given()
                .when().get()
                .then()
                .statusCode(200)
                .body("$.size()", is(3),
                        "[0].id", is(1),
                        "[0].name", is("Batman"),
                        "[1].id", is(2),
                        "[1].name", is("Superman"),
                        "[2].id", is(3),
                        "[2].name", is("Wonder woman")
                );

Since our customer HTTP GET returns an array of Customer objects, we can check each element of the array using the [n] index and the class property. Additionally, we are also asserting that the number of elements returned equals to 3.

Then, notice how we can easily post a Java object into the body by setting the content type accordingly (in our case JSON):

        given()
                .contentType("application/json")
                .body(new Customer("Iron man"))
                .when()
                .post()
                .then()
                .statusCode(201);

Testing an HTTP Resource of your Quarkus application

The next test example shows how to check the content of a Web resource using the @TestHTTPResource annotation. This annotation is bound to a java.net.URL resource. Therefore you can read the HTTP resource as an InputStream and verify the content of the page. In this example, we are checking that the HTML page contains the title “Quarkus-hibernate example”:

@QuarkusTest
public class CustomerEndpointHTTPTest {
    @TestHTTPResource("index.html")
    URL url;

    @Test
    public void testIndexHtml() throws Exception {
        try (InputStream in = url.openStream()) {
            String contents = readStream(in);
            Assertions.assertTrue(contents.contains("<title>Quarkus-hibernate example</title>"));
        }
    }

    private static String readStream(InputStream in) throws IOException {
        byte[] data = new byte[1024];
        int r;
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        while ((r = in.read(data)) > 0) {
            out.write(data, 0, r);
        }
        return new String(out.toByteArray(), StandardCharsets.UTF_8);
    }
}

Adding callbacks for each test

You can receive callbacks each time a single test is run by implementing the QuarkusTestBeforeEachCallback and QuarkusTestAfterEachCallback interface. See the following example:

import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback;
import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;

public class TestCallBackExample implements QuarkusTestBeforeEachCallback, QuarkusTestAfterEachCallback {

    public void beforeEach(QuarkusTestMethodContext context) {
        System.out.println("Executing " + context.getTestMethod());
    }
    public void afterEach(QuarkusTestMethodContext context) {
        System.out.println("Executed " + context.getTestMethod());
    }
}

In order to register the CallBack classes, you have to add a file under the folder resources/META-INF/services with the name of the interface:

$ tree src/main/resources/
src/main/resources/
├── application.properties
├── import.sql
└── META-INF
    ├── resources
    │   └── index.html
    └── services
        ├── io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback
        └── io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback

Within each file, you need to specify the implementing class, which is in our example (for both files):

org.acme.TestCallBackExample

Using a specific profile for your Quarkus Tests

Out of the box Quarkus includes the following configuration profiles:

  • dev – Triggered when running in development mode (i.e. quarkus:dev)
  • test – Triggered when running tests
  • prod – This is picked up when not running in development or test mode

You can use the following syntax to bind a configuration parameter to a specific profile:

%{profile}.config.key=value

As an example, you can use the following application.properties configuration file, which defines multiple profiles in it:

%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgresDev
%test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:6432/postgresTest
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:7432/postgresProd

In the above configuration, we have specified three different JDBC URLs for our Datasource connection. Each one is bound to a different profile. This way, you can use a configuration specific for the test profile.

Besides that, you can also define custom profiles by implementing the io.quarkus.test.junit.QuarkusTestProfile as in this example:

import io.quarkus.test.junit.QuarkusTestProfile;

public class CustomProfile implements  QuarkusTestProfile {

    @Override
    public Map<String, String> getConfigOverrides() {
        return Map.of("message", "Hi there!");
    }

    @Override
    public String getConfigProfile() {
        return "custom-profile";
    }
}

To use the custom profile in your tests, just add the @TestProfile with a reference to your Profile class:

import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import org.junit.jupiter.api.Test;

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

@QuarkusTest
@TestHTTPEndpoint(CustomerResource.class)
@TestProfile(CustomProfile.class)
public class CustomProfileTest {

    @Test
    public void testCustomProfile() {

        given()
                .when().get("/hello")
                .then()
                .statusCode(200)
                .body(is("Hi there!"));


    }
}

How to plug a Database in your Integration tests

Our example application uses a Database for reading/writing data. The simplest way to perform integration tests with a Database is to run the DB as Docker process. If you don’t want to manually start and stop your container, you an use the “docker-maven-plugin” which lets you bind the start and stop of the Docker image accordingly with the Maven execution. Here is how to add PostgreSQL to our test example:

      <plugin>
        <!-- Automatically start PostgreSQL for integration testing - requires Docker -->
        <groupId>io.fabric8</groupId>
        <artifactId>docker-maven-plugin</artifactId>
        <version>${docker-plugin.version}</version>
        <configuration>
          <skip>false</skip>
          <images>
            <image>
              <name>postgres:10.5</name>
              <alias>postgresql</alias>
              <run>
                <env>
                  <POSTGRES_USER>quarkus</POSTGRES_USER>
                  <POSTGRES_PASSWORD>quarkus</POSTGRES_PASSWORD>
                  <POSTGRES_DB>quarkusdb</POSTGRES_DB>
                </env>
                <ports>
                  <port>5432:5432</port>
                </ports>
                <log>
                  <prefix>PostgreSQL: </prefix>
                  <date>default</date>
                  <color>cyan</color>
                </log>
                <wait>
                  <tcp>
                    <mode>mapped</mode>
                    <ports>
                      <port>5432</port>
                    </ports>
                  </tcp>
                  <time>10000</time>
                </wait>
              </run>
            </image>
          </images>
        </configuration>
        <executions>
          <execution>
            <id>docker-start</id>
            <phase>test-compile</phase>
            <goals>
              <goal>stop</goal>
              <goal>start</goal>
            </goals>
          </execution>
          <execution>
            <id>docker-stop</id>
            <phase>post-integration-test</phase>
            <goals>
              <goal>stop</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

The source code for this example is available: https://github.com/fmarchioni/mastertheboss/tree/master/quarkus/testing