It’s been a long time since my last post as a lot has happened in my life. I’ve changed jobs and my time hasn’t been the same.

In this post I’ll explain how to use Testcontainers with quarkus.

Requirements for this post

  • Docker version 1.6+
  • Maven
  • Java 11

Motivation for using Testcontainers

Usually, when developing with java in a test-driven-development environment, you usually opt to use H2 in memory database, but that decision brings with it some issues:

  • You are not testing against your usual production Database
  • You end up dual sql syntax, one for tests and other for production
  • You fail to see the issues before they happen

Enter Testcontainers!!

Since this post is for an hello world application, lets start by creating our project.

Quarkus Setup

So, we will be using a simple project with maven archetype for quarkus. This is for simplicity, but you may use quarkus web interface to generate a scaffolded project with your selected dependencies.

mvn io.quarkus:quarkus-maven-plugin:1.2.0.Final:create \
    -DprojectGroupId=com \
    -DprojectArtifactId=ilhicas \
    -DprojectVersion=1.0.0 \
    -DclassName="com.ilhicas.QuarkusTestContainers"

This should take a few moments, to scaffold the project.

So now lets open QuarkusTestContainers.java and create our only endpoint just to have something to test with.

package com.ilhicas;

import java.time.LocalDate;
import java.time.Month;

import javax.transaction.Transactional;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import com.ilhicas.entities.Person;

@Path("/person")
public class QuarkusTestContainers {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }

    @POST
    @Transactional
    @Produces(MediaType.TEXT_PLAIN)
    public Person createHello() {
        Person p = new Person();
        p.name =  "ilhicas";
        p.birth = LocalDate.of(1910, Month.FEBRUARY, 1);
        p.persist();
        return p;
    }
}

Ok, so now we are already using @Transaction, and we will be using Panache for our ORM with an Entity named Person that we will need to create.

We will also use postgresql for our project, so we need both dependencies.

Let’s first edit our pom to include panache.

    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-hibernate-orm-panache</artifactId>
      <version>1.3.0.Alpha1</version>
    </dependency>
      <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-jdbc-postgresql</artifactId>
      </dependency>

Ok, so now that we have our dependencies in place, let’s add our entity.

package com.ilhicas.entities;

import java.time.LocalDate;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Person extends PanacheEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;

    public String name;
    public LocalDate birth;
}

It’s a simple entity as our point is not to actually create a Quarkus project, but rather a way to test with Testcontainers for our database.

So since our whole point is Testcontainers, we must add it to our pom.xml now so that we may have the required dependencies to run a Postgres container before our application starts in our tests.

    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>1.12.5</version>
    </dependency>
  
    <dependency>
      <groupId>org.testcontainers</groupId>
      <artifactId>postgresql</artifactId>
      <version>1.12.5</version>
    </dependency>

org.testcontainers.junit-jupiter grants us some neat annotations, and the postgresql a specialized Testcontainers class for Postgresql(Name is pretty suggesting :D )

Now lets edit the Test file named QuarkusTestContainersTest that the archetype scaffold has created for us.

package com.ilhicas;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

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

import com.github.dockerjava.api.model.ExposedPort;
import com.github.dockerjava.api.model.PortBinding;
import com.github.dockerjava.api.model.Ports;

@Testcontainers
@QuarkusTest
class QuarkusTestContainersTest {

    @Container
    static PostgreSQLContainer db = new PostgreSQLContainer<>("postgres:11-alpine")
            .withDatabaseName("testcontainers")
            .withUsername("andre")
            .withPassword("ilhicas")
            .withExposedPorts(5432)
            // Below should not be used - Function is deprecated and for simplicity of test , You should override your properties at runtime
            .withCreateContainerCmdModifier(cmd -> {
                cmd
                        .withHostName("localhost")
                        .withPortBindings(new PortBinding(Ports.Binding.bindPort(5432), new ExposedPort(5432)));
            });

    
    @Test
    public void testGETHelloEndpoint() {
        given()
          .when().get("/person")
          .then()
             .statusCode(200)
             .body(is("hello"));
    }

    @Test
    public void testPOSTHelloEndpoint() {
        given()
          .when().post("/person")
          .then()
             .statusCode(200);
    }

}

The file should actually be named QuarkusTestContainersIT since its an integration test, but for simplicity let’s keep it that way.

So, now we see we added two annotation that don’t belong to Quarkus:

  • @Testcontainers - Its supposed to be before @QuarkusTest
  • @Container - It’s supposed to be a static container instance

The @Testcontainers marks the test to use containers definitions to run before quarkus starts its application.

The @Container application creates a singleton object that will be available through all our tests in this class.

Lifecycle for container is as follows:

  • Container starts before quarkus, and since we are using a special container class, it will wait until port is open
  • Container is destroyed after all tests declared in class run.
  • A reaper container is also created by Testcontainers to allow for destruction of assets (containers) that might get stuck due to cancelled tests, other sort of failures etc..

We also must make sure to update our src/test/java/resources/application.properties to match our test in this case.

quarkus.datasource.url = jdbc:postgresql://localhost:5432/testcontainers
quarkus.datasource.driver = org.postgresql.Driver
quarkus.datasource.username = andre
quarkus.datasource.password = ilhicas
quarkus.hibernate-orm.database.generation = drop-and-create

So, now let’s run our test:

mvn test

Or just run directly from you favourite IDE

We should also see something similar to the following logs in our terminal or IDE:

INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.ilhicas.QuarkusTestContainersTest
        ℹ︎ Checking the system...
        ✔ Docker version should be at least 1.6.0
        ✔ Docker environment should have more than 2GB free disk space
2020-02-13 04:18:33,273 INFO  [org.tes.doc.DockerClientProviderStrategy] (main) Loaded org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrategy from ~/.testcontainers.properties, will try it first
2020-02-13 04:18:36,257 INFO  [org.tes.doc.EnvironmentAndSystemPropertyClientProviderStrategy] (main) Found docker client settings from environment
2020-02-13 04:18:36,258 INFO  [org.tes.doc.DockerClientProviderStrategy] (main) Found Docker environment with Environment variables, system properties and defaults. Resolved dockerHost=unix:///var/run/docker.sock
2020-02-13 04:18:36,942 INFO  [org.tes.DockerClientFactory] (main) Docker host IP address is localhost
2020-02-13 04:18:37,062 INFO  [org.tes.DockerClientFactory] (main) Connected to docker:
  Server Version: 19.03.5
  API Version: 1.40
  Operating System: Ubuntu 18.04.2 LTS
  Total Memory: 4636 MB

I’m actually running this from WSL2 with Windows Home Edition

Final Notes:

For the purpose of this test we fixed our port binding to 5432, but you should make sure you let Docker choose a random port, and just change your application properties dynamically @BeforeAll as to make sure its changed before application starts.

PostgreSQLContainer also grants us a nice method to retrieve our current jdbc url for our newly created container.

If you want, when running the test from IDE you may set a debug point and postgres container will keep running, you may enter your container and check created tables, entries, attach a pgadmin if you wish, etc..

And that’s it for this post. Hope you enjoyed or find it somehow helpful. Please share, if you wish.

As usual, a github companion repository has been created with the project used for this post. testcontainers with quarkus

André Ilhicas dos Santos

Devops Padawan, curious about systems automation, learning new languages, paradigms tools each day.

ilhicas ilhicas


Published