Development and Testing of Quarkus applications using Testcontainers
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
Java 17+
Your favorite IDE (Intellij IDEA, Eclipse, NetBeans, VS Code)
A Docker environment supported by Testcontainers https://www.testcontainers.org/supported_docker_environment/
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.
For 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:
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.
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
It 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