Advanced Testing with Quarkus

Advanced Testing with Quarkus

This article will teach you how to build advanced testing scenarios with Quarkus. We will focus mainly on the integration tests. Quarkus simplifies them by leveraging the Testcontainers project. In many cases, it is a smooth integration process. You won’t even notice you are using Testcontainers under the hood.

Before starting with the test it is worth reading about the Quarkus framework. If you are familiar with Spring Boot, I especially recommend the following article about Quarkus. It shows some useful and interesting features of the Quarkus framework that Spring Boot doesn’t provide.

Introduction: The Basics

Let’s begin with the basics. Quarkus has three different launch modes: dev, test, and prod. It defines a built-in profile for each of those modes. As you probably guessed, today we will focus on the test mode. It is automatically activated when running tests during the build. The class containing tests should be annotated with @QuarkusTest. We may provide a configuration dedicated to the particular mode using the following semantics in the application.properties file:

%prod.quarkus.datasource.db-kind = postgresql
%prod.quarkus.datasource.username = ${PG_USER}
%prod.quarkus.datasource.password = ${PG_PASS}
%prod.quarkus.datasource.jdbc.url = jdbc:postgresql://pg:5432/${PG_DB}

Let’s assume we have a very simple endpoint that returns an object by its id:

@Path("/persons")
public class PersonResource {

   @Inject
   InMemoryPersonRepository inMemoryRepository;

   @GET
   @Path("/{id}")
   public Person getPersonById(@PathParam("id") Long id) {
      return inMemoryRepository.findById(id);
   }

}

Now, we have to create a test. We don’t need to take care of a port. By default, Quarkus runs on the 8081 port in test mode. It also automatically configures the Rest Assured library to interact with the server.

@QuarkusTest
public class PersonResourceTests {

   @Test
   void getById() {
      given().get("/persons/{id}", 1L)
         .then()
         .statusCode(200)
         .body("id", notNullValue());
   }

}

Source Code

If you would like to try it by yourself, you may always take a look at my source code. This time, we have multiple repositories with examples. All those repositories contain Quarkus testing scenarios for the different use cases. You can clone the repository with a single app that connects to the Postgres database. There are two other repositories with microservices. Here’s the repository with simple microservices. There is another one with Consul configuration and discovery. Then you should just follow my instructions.

Testing with External Services

Let’s include a database in our scenario. We will use Postgres. In order to interact with the database, we will leverage the Quarkus Panache ORM module. Firstly, we need to add the following two dependencies:

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

Here’s our Person entity:

@Entity
public class Person extends PanacheEntityBase {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   public Long id;
   public String name;
   public int age;
   @Enumerated(EnumType.STRING)
   public Gender gender;

}

We also need to create the repository:

@ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {

   public List<Person> findByName(String name) {
      return find("name", name).list();
   }

   public List<Person> findByAgeGreaterThan(int age) {
      return find("age > ?1", age).list();
   }

}

Finally, we have a resource endpoints implementation:

@Path("/persons")
public class PersonResource {

   private PersonRepository repository;

   public PersonResource(PersonRepository repository) {
      this.repository = repository;
   }

   @GET
   public List<Person> findAll() {
      return repository.findAll().list();
   }

   @GET
   @Path("/name/{name}")
   public List<Person> findByName(@PathParam("name") String name) {
      return repository.findByName(name);
   }

   @GET
   @Path("/{id}")
   public Person findById(@PathParam("id") Long id) {
      return repository.findById(id);
   }
}

The most important thing here is not to place any addresses for the test mode. We may set them, for example, only for the prod mode. Here’s our test. It doesn’t differ much from the previous one. We don’t need to add any special annotations, dependencies, or objects. Everything happens automatically. The only thing we need to guarantee is access to the Docker host. Quarkus will automatically start the Postgres container there and configure connection settings for the app.

@QuarkusTest
public class PersonResourceTest {

    @Test
    public void findAll() {
        given()
          .when().get("/persons")
          .then()
             .statusCode(200)
             .assertThat().body("size()", is(20));
    }

    @Test
    public void findById() {
        Person person = given()
                .when().get("/persons/1")
                .then()
                .statusCode(200)
                .extract()
                .body().as(Person.class);
        assertNotNull(person);
        assertEquals(1L, person.id);
    }

}

Let’s run our tests. Here’s the fragment of the logs. Before running the tests Quarkus starts the Postgres container using Testcontainers:

quarkus-testing-postgres

Then it runs our tests and exposes the app at the 8081 port.

Ok, our tests work fine on the local machine. However, the goal is to run them as a part of the CI process. This time we will use CircleCI. The process needs to have access to the Docker host. We may use a dedicated Linux machine as an executor or take advantage of the Testcontainers cloud. Here’s a build configuration for the second option.

version: 2.1

orbs:
  maven: circleci/maven@1.4.0
  tcc: atomicjar/testcontainers-cloud-orb@0.1.0

executors:
  j17:
    docker:
      - image: 'cimg/openjdk:17.0'

workflows:
  maven_test:
    jobs:
      - maven/test:
          executor: j17
          context: Testcontainers
          pre-steps:
            - tcc/setup

Now, we just need to create a job in CircleCI and run the pipeline.

Integration Testing with Quarkus

Instead of @QuarkusTest, we can annotate our test with @QuarkusIntegrationTest. It is a really powerful solution in conjunction with the Quarkus containers build feature. It allows us to run the tests against an already-built image containing the app. First, let’s include the Quarkus Jib module:

<dependency>
   <groupId>io.quarkus</groupId>
   <artifactId>quarkus-container-image-jib</artifactId>
</dependency>

We need to enable container build in the application.properties:

quarkus.container-image.build = true

This time instead of the RestAssured object we will use the real HTTP client. Quarkus provides a convenient method for creating declarative REST clients. We need to define an interface with endpoint methods:

public interface EmployeeService {

   @POST
   Employee add(@Valid Employee employee);

   @GET
   Set<Employee> findAll();

   @Path("/{id}")
   @GET
   Employee findById(@PathParam("id") Long id);

}

In the test class, we use @TestHTTPResource and @TestHTTPEndpoint annotations to inject the test URL. Then we are creating a client with the RestClientBuilder and call the service started on the container. The name of the test class is not accidental. In order to be automatically detected as the integration test, it has the IT suffix.

@QuarkusIntegrationTest
public class EmployeeControllerIT {

    @TestHTTPEndpoint(EmployeeController.class)
    @TestHTTPResource
    URL url;

    @Test
    void add() {
        EmployeeService service = RestClientBuilder.newBuilder()
                .baseUrl(url)
                .build(EmployeeService.class);
        Employee employee = new Employee(1L, 1L, "Josh Stevens", 
                                         23, "Developer");
        employee = service.add(employee);
        assertNotNull(employee.getId());
    }

    @Test
    public void findAll() {
        EmployeeService service = RestClientBuilder.newBuilder()
                .baseUrl(url)
                .build(EmployeeService.class);
        Set<Employee> employees = service.findAll();
        assertTrue(employees.size() >= 3);
    }

    @Test
    public void findById() {
        EmployeeService service = RestClientBuilder.newBuilder()
                .baseUrl(url)
                .build(EmployeeService.class);
        Employee employee = service.findById(1L);
        assertNotNull(employee.getId());
    }
}

Now, we just need to include the maven-failsafe-plugin. It will run our test during the verify or integration-test Maven phase.

<plugin>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>${surefire-plugin.version}</version>
  <executions>
    <execution>
      <goals>
        <goal>integration-test</goal>
        <goal>verify</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Let’s see how it works. In order to run the tests we need to execute the following Maven command:

$ mvn clean verify

Before running the tests, Quarkus build the app image using Jib:

Then, Maven runs the integration tests. Quarkus app starts as the container on Docker and exposes its endpoint over the default test URL http://localhost:8081.

quarkus-testing-integration

Testcontainers with Quarkus 

By default, Quarkus automatically runs several third-party services as containers. It includes databases, brokers like Kafka or RabbitMQ, and some others tools. Here’s a full list of supported software. What if we have a tool that’s not on that list? Let’s consider HashiCorp Consul. There’s a dedicated module for integrating it via Testcontainers:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>consul</artifactId>
  <version>1.17.6</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>1.17.6</version>
  <scope>test</scope>
</dependency>

In order to start the container during the test we need to create a class that implements the QuarkusTestResourceLifecycleManager interface. It defines two methods: start() and stop(). Inside the start() method, we are creating and running the Consul container. Once it starts successfully we put a new key department.name under the config/department path. Then we need to override the address of the Consul used by the test to the dynamic address of the container run by Testcontainers.

public class ConsulResource implements QuarkusTestResourceLifecycleManager {

   private ConsulContainer consul;

   @Override
   public Map<String, String> start() {
      consul = new ConsulContainer("consul:1.14")
         .withConsulCommand("kv put config/department department.name=abc");

      consul.start();

      String url = consul.getHost() + ":" + consul.getFirstMappedPort();

      return ImmutableMap.of("quarkus.consul-config.agent.host-port", url);
   }

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

Here are the application settings related to the Consul instance. We use them only for storing configuration keys and values.

quarkus.consul-config.enabled=true
quarkus.consul-config.properties-value-keys=config/${quarkus.application.name}

Finally, we can go to the test implementation. In order to start the Consul container defined inside the ConsulResource class during the test we need to annotate the whole test with @QuarkusTestResource. By default, all test resources are global, even if they are defined on a test class or custom profile, which means 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 need to set the restrictToAnnotatedClass field to true. In the following test, I’m injecting the property department.name defined in our Consul instance under the /config/department key.

@QuarkusTest
@QuarkusTestResource(ConsulResource.class, 
                     restrictToAnnotatedClass = true)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class DepartmentResourceConsulTests {

   @ConfigProperty(name = "department.name", defaultValue = "")
   private String name;

   @Test
   @Order(1)
   void add() {
      Department d = new Department();
      d.setOrganizationId(1L);
      d.setName(name);

      given().body(d).contentType(ContentType.JSON)
              .when().post("/departments").then()
              .statusCode(200)
              .body("id", notNullValue());
   }

   @Test
   @Order(2)
   void findAll() {
       when().get("/departments").then()
              .statusCode(200)
              .body("size()", is(1));
   }
}

Once again let’s run the test using the mvn clean verify command. Of course, don’t forget to run Docker on your laptop. Then, you verify the result.

Enable Profiles

Instead of creating an integration test that runs the Consul container, we can create a simple unit test with disabled interaction with Consul. In order to do that, we need to override a configuration setting. Of course, we can do it globally for the test profile using the following notation:

%test.quarkus.consul-config.enabled = false

However, assuming there are several different tests in the project, we may a different set of configuration properties per a single test class. In that case, we still have the integration test that interacts with the Consul container started on Docker. For such a scenario, Quarkus provides the QuarkusTestProfile interface. We need to create a class that implements it and overrides the value of the quarkus.consul-config.enabled property inside the getConfigOverrides() method.

public class DisableExternalProfile implements QuarkusTestProfile {

    @Override
    public Map<String, String> getConfigOverrides() {
        return Map.of("quarkus.consul-config.enabled", "false");
    }
}

Then, we just need to annotate the test class with the @TestProfile holding of our implementation of the QuarkusTestProfile interface.

@QuarkusTest
@TestProfile(DisableExternalProfile.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class DepartmentResourceTests {

    @Test
    @Order(1)
    void add() {
        Department d = new Department();
        d.setName("test");
        d.setOrganizationId(1L);

        given().body(d).contentType(ContentType.JSON)
                .when().post("/departments").then()
                .statusCode(200)
                .body("id", notNullValue());
    }

    @Test
    @Order(2)
    void findAll() {
        when().get("/departments").then()
                .statusCode(200)
                .body("size()", is(1));
    }

    @Test
    @Order(2)
    void findById() {
        when().get("/departments/{id}", 1).then()
                .statusCode(200)
                .body("id", is(1));
    }

}

Testing with Quarkus on Kubernetes

This topic couldn’t be missed in my article. The last part of the article will show how to deploy the app on Kubernetes and run tests against an application pod. There is no built-in support in Quarkus for the whole scenario, but we can simplify it with some separate features. First of all, Quarkus provides built-in support for building the image (we have already done it in one of the previous sections) and generating YAML manifests for Kubernetes. In order to use we need to include Quarkus Kubernetes Extension in the Maven dependencies. We will also include the fabric8 Kubernetes client for the test purposes:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-kubernetes</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-kubernetes-client</artifactId>
  <scope>test</scope>
</dependency>

Now, if we set the property quarkus.kubernetes.deploy to true during the build, Quarkus will try to deploy the image to the current Kubernetes cluster. We can customize this process using Quarkus configuration properties. Of course, we need to build the image and push it to the container registry. In order to easily test the app in the @QuarkusIntegrationTest we will enable the NodePort Kubernetes service. Here’s the full set of configuration properties to perform the test.

quarkus.container-image.build = true
quarkus.container-image.group = piomin
quarkus.container-image.push = true

quarkus.kubernetes.deploy = true
quarkus.kubernetes.namespace = default
quarkus.kubernetes.service-type = node-port

It is important to run the test after the Maven build phase to deploy an already created and pushed image. The same in one of the previous scenarios we can use the @QuarkusIntegrationTest for that. We can use the KubernetesClient to detect the target port the employee-service running on Kubernetes. Then, we will use the Quarkus Rest client to call the target service as shown below.

@QuarkusIntegrationTest
public class EmployeeAppKubernetesIT {

   KubernetesClient client = new KubernetesClientBuilder().build();

   @Test
   void api() throws MalformedURLException {
      Service service = client.services()
         .inNamespace("default")
         .withName("employee-service")
         .get();
      ServicePort port = service.getSpec().getPorts().get(0);
      EmployeeService client = RestClientBuilder.newBuilder()
         .baseUrl(new URL("https://localhost:" + port.getNodePort() + "/employees"))
         .build(EmployeeService.class);
      Employee employee = new Employee(1L, 1L, "Josh Stevens", 23, "Developer");
      employee = client.add(employee);
      assertNotNull(employee.getId());
   }
}

That’s it. Let’s run the test. In the first step, the Quarkus Maven plugin builds the app image using Jib and pushes it to the registry:

Only after that, it tries to deploy the image on the current Kubernetes cluster. It creates Deployment and Service with the NodePort type.

quarkus-testing-kubernetes

Finally, it will run the test against the current Kubernetes cluster. As I mentioned before, there is no full built-in support for that scenario. So, for example, Quarkus still tries to run the Docker container with the app. In this scenario, our test ignores it and connects to the app deployed on Kubernetes.

Final Thoughts

Quarkus simplifies several things in automation testing. It effectively uses containers in the integration tests. Most of the things work out of the box without any additional configuration or annotations. Finally, we can easily include Kubernetes in our testing scenarios thanks to Quarkus Kubernetes Extension. I just included the most interesting Quarkus testing features. For detailed pieces of information, you may refer to the docs.

6 COMMENTS

comments user
GG

Nice post. I will try the sample project.

    comments user
    piotr.minkowski

    Thanks!

comments user
Lorival W. M. (@lorivalmatias)

Great article.

A suggestion for another one: Testing with multiple websockets, would be great for me, I am facing problems with static queues.

    comments user
    piotr.minkowski

    Ok, I’ll think about it. I’m not very strong in web sockets. I would probably have to do some research before

comments user
Dmitrii

Hello,
do I understand correctly that QuarkusIntegrationTest annotation has Testcontainers annotation under the hood? And we can do all the magic with starting external containers inside our tests?

    comments user
    piotr.minkowski

    Hi,
    In general, quarkusn uses testcontainers automatically for several technologies under the hood – you don’t even need QuarkusIntegrationTest for that.

Leave a Reply