Development and Testing of Quarkus applications using Testcontainers

  • Java
  • Quarkus
  • PostgreSQL
  • REST Assured
Get the code

In this guide, you will learn how to

  • Create a Quarkus application

  • Implement REST API endpoints using JAX-RS, Hibernate ORM with Panache and PostgreSQL

  • Test the REST API using Testcontainers and RestAssured.

Prerequisites

What we are going to achieve in this guide

We are going to create a Quarkus application using Hibernate ORM with Panache together with a Postgres database and implement a couple of REST API endpoints. Then we will test these API endpoints using RestAssured and Quarkus Dev Services, which uses Testcontainers behind the scenes.

We will also learn how to test your application using the Docker containers that are not supported by Quarkus Dev Services out of the box. Finally, we will learn how to run Quarkus applications locally using Dev Services.

Getting Started

Create a new Quarkus application from https://code.quarkus.io/ by selecting the RESTEasy Classic, RESTEasy Classic Jackson, Hibernate Validator, Hibernate ORM with Panache, JDBC Driver - PostgreSQL, and Flyway extensions.

We are going to implement two REST API endpoints, one to fetch all the customers and another one to create a new customer. We will use JAX-RS with RESTEasy Classic to implement API handlers and Hibernate ORM with Panache for persistence.

Hibernate ORM with Panache supports the Active Record Pattern and Repository Pattern to simplify JPA usage. In this guide, we will use the Active Record Pattern.

Create JPA entity

First, let us start with creating a JPA entity Customer by extending the PanacheEntity class.

package com.testcontainers.demo;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

@Entity
@Table(name = "customers")
public class Customer extends PanacheEntity {

    @Column(nullable = false)
    public String name;

    @Column(nullable = false, unique = true)
    public String email;

    public Customer() {}

    public Customer(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

By extending the PanacheEntity class, we will have various persistence methods such as persist(), listAll(), findById() etc. available on Customer class.

Create CustomerService CDI Bean

Create a CustomerService class to perform various operations on the Customer entity. We will mark CustomerService as a CDI bean by annotating it with @ApplicationScoped and also make its public methods to work within a transactional boundary by annotating it with @Transactional.

package com.testcontainers.demo;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import java.util.List;

@ApplicationScoped
@Transactional
public class CustomerService {

    public List<Customer> getAll() {
        return Customer.listAll();
    }

    public Customer create(Customer customer) {
        customer.persist();
        return customer;
    }
}

Add Flyway database migration script

We are going to use the Flyway database migration tool to perform database migrations.

Create a V1__init_database.sql file with the following content under the src/main/resources/db/migration directory.

create sequence customers_seq start with 1 increment by 50;

create table customers
(
    id    bigint DEFAULT nextval('customers_seq') not null,
    name  varchar                                 not null,
    email varchar                                 not null,
    primary key (id)
);

insert into customers(name, email)
values ('john', 'john@mail.com'),
       ('rambo', 'rambo@mail.com');

We also need to enable the execution of flyway migrations by adding the following property in the src/main/resources/application.properties file.

quarkus.flyway.migrate-at-start=true

Implement REST API endpoints

Finally, create a CustomerResource class to implement a REST API endpoints to fetch all customers and create a new customer as follows:

package com.testcontainers.demo;

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;

@Path("/api/customers")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CustomerResource {
    private final CustomerService customerService;

    public CustomerResource(CustomerService customerService) {
        this.customerService = customerService;
    }

    @GET
    public List<Customer> getAllCustomers() {
        return customerService.getAll();
    }

    @POST
    public Response createCustomer(Customer customer) {
        var savedCustomer = customerService.create(customer);
        return Response.status(Response.Status.CREATED).entity(savedCustomer).build();
    }
}

Introducing Quarkus Dev Services

Quarkus Dev Services supports the automatic provisioning of unconfigured services in development and test mode. If we include an extension and don’t configure it then Quarkus will automatically start the relevant service, usually using Testcontainers behind the scenes, and wire up your application to use this service.

NoteFor Dev Services to work, you need to have a Supporting Docker environment available.

Quarkus Dev Services provide support for most commonly used services like SQL databases, Kafka, RabbitMQ, Redis, MongoDB etc.

For more information on Quarkus Dev Services, read https://quarkus.io/guides/dev-services

Write Tests for API endpoints

We are going to write tests for the GET /api/customers and POST /api/customers API endpoints using RestAssured.

When we generated the application, the io.rest-assured:rest-assured library was already added as a test dependency.

Now, we can create a CustomerResourceTest class and annotate it with @QuarkusTest which will bootstrap the application along with the required services using Dev Services. As we haven’t configured the datasource properties, Dev Services will automatically start a PostgreSQL database using Testcontainers.

package com.testcontainers.demo;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.common.mapper.TypeRef;
import io.restassured.http.ContentType;
import java.util.List;
import org.junit.jupiter.api.Test;

@QuarkusTest
class CustomerResourceTest {

    @Test
    void shouldGetAllCustomers() {
        List<Customer> customers = given().when()
                .get("/api/customers")
                .then()
                .statusCode(200)
                .extract()
                .as(new TypeRef<>() {});
        assertFalse(customers.isEmpty());
    }

    @Test
    void shouldCreateCustomerSuccessfully() {
        Customer customer = new Customer(null, "John", "john@gmail.com");
        given().contentType(ContentType.JSON)
                .body(customer)
                .when()
                .post("/api/customers")
                .then()
                .statusCode(201)
                .body("name", is("John"))
                .body("email", is("john@gmail.com"));
    }
}

Now if you run the test, you should see in the console logs that a Postgres database container is automatically started, Flyway migrations are applied, and your tests are executed using that database successfully.

However, you can notice two things here:

  1. While tests are running, your application started on port 8081. It would be better to start on a randomly available port so that there won’t be any port conflicts.

  2. By default, the Postgres database container is started using a postgres:14 docker image. You might want to change the docker image tag to use a specific PostgreSQL version.

We can customize both of these behaviors by adding the following properties in src/main/resources/application.properties file.

quarkus.http.test-port=0
quarkus.datasource.devservices.image-name=postgres:15.2-alpine
TipIt is highly recommended to explicitly configure the docker container image versions(tags) to match with the versions of the services you are running in production.

Now if you run the test, the Quarkus application will be started on a random available port and the Dev Services create a Postgres container based on the postgres:15.2-alpine docker image.

Run tests

# If you are using Maven
./mvnw test

# If you are using Gradle
./gradlew test

You should see the Postgres docker container is started and all tests should PASS.

How to test with services not supported by Dev Services?

Your application may be using a service that is not supported by Dev Services out of the box. In such cases, you can use QuarkusTestResource and QuarkusTestResourceLifecycleManager to start the required services before the Quarkus application starts for testing.

Let’s suppose we are using CockroachDB in our application, and as of now, Dev Services doesn’t support CockroachDB out of the box. But we can still start and configure CockroachDB using Testcontainers and QuarkusTestResourceLifecycleManager automatically before executing the tests.

First, let’s add the cockroachdb Testcontainers module dependency.

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>cockroachdb</artifactId>
    <version>1.18.1</version>
    <scope>test</scope>
</dependency>

Create a CockroachDBTestResource by implementing the QuarkusTestResourceLifecycleManager interface as follows:

package com.testcontainers.demo;

import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import java.util.HashMap;
import java.util.Map;
import org.testcontainers.containers.CockroachContainer;

public class CockroachDBTestResource implements QuarkusTestResourceLifecycleManager {

    CockroachContainer cockroachdb;

    @Override
    public Map<String, String> start() {
        cockroachdb = new CockroachContainer("cockroachdb/cockroach:v22.2.0");
        cockroachdb.start();
        Map<String, String> conf = new HashMap<>();
        conf.put("quarkus.datasource.jdbc.url", cockroachdb.getJdbcUrl());
        conf.put("quarkus.datasource.username", cockroachdb.getUsername());
        conf.put("quarkus.datasource.password", cockroachdb.getPassword());
        return conf;
    }

    @Override
    public void stop() {
        cockroachdb.stop();
    }
}

Next, we can use the CockroachDBTestResource by using @QuarkusTestResource as follows:

package com.testcontainers.demo;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertFalse;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.common.mapper.TypeRef;
import java.util.List;
import org.junit.jupiter.api.Test;

@QuarkusTest
@QuarkusTestResource(value = CockroachDBTestResource.class, restrictToAnnotatedClass = true)
class CockroachDBTest {

    @Test
    void shouldGetAllCustomers() {
        List<Customer> customers = given().when()
                .get("/api/customers")
                .then()
                .statusCode(200)
                .extract()
                .as(new TypeRef<>() {});
        assertFalse(customers.isEmpty());
    }
}

If you run this test, you can see that the cockroachdb container is started before running the test and your application datasource is configured to use the cockroachdb.

By default, test resources are global, which means even if they are defined on a test class or custom profile, they will all be activated for all tests. If you want to only enable a test resource on a single test class or test profile, you can use @QuarkusTestResource(restrictToAnnotatedClass = true).

So, we have added restrictToAnnotatedClass = true attribute to start the cockroachdb only while running this CockroachDBTest test class.

Run application locally

As mentioned previously, Quarkus Dev Services automatically provision the unconfigured services in development and test mode.

We can start the Quarkus application in dev mode using ./mvnw compile quarkus:dev or ./gradlew quarkusDev which will automatically start the unconfigured services, Postgres in our case, using Dev Services.

# If you are using Maven
./mvnw compile quarkus:dev

# If you are using Gradle
./gradlew quarkusDev

If you are running a Postgres database on your system and want to use that database during development, you can configure datasource properties in src/main/resources/application.properties file as follows:

quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres
quarkus.datasource.username=postgres
quarkus.datasource.password=postgres

When you explicitly configure these properties, Dev Services will not provision the database container, instead it will connect to the database using the configured properties.

Summary

Quarkus improved the developer experience greatly with Dev Services by automatically provisioning the required services using Testcontainers during development and testing.

We have learned how to implement REST API using JAX-RS together with Hibernate ORM with Panache. Then we tested the API endpoints using RestAssured by leveraging the Dev Services. We also learned how to use the services that are not yet supported by Dev Services using QuarkusTestResourceLifecycleManager. Finally, we learned how to run the application locally using Dev Services and externally installed service as well.

The seamless integration with Testcontainers helped us to build and test the Quarkus applications without requiring explicit configuration.

To learn more about Testcontainers visit http://testcontainers.com