1. Overview

In this article, we’ll see the power of using Testcontainers to help test a Quarkus application using ‘live’ service-to-service testing.

Testing in a microservices architecture is important and it can be difficult to reproduce a production-like system. Common options to enable testing include using manual API testing tools such as Postman, mocking services, or trusting in-memory databases. We might not perform a real service-to-service interaction until running a CI pipeline or deploying to upper environments. This can pose a problem, and delay delivery, by not thoroughly testing service interactions.

Testcontainers provides a solution to this by granting the ability to spin up dependencies and test against them directly in our local environment. This allows us to leverage any containerized application, service, or dependency and use in our test cases.

2. Solution Architecture

Our solution architecture is rather simple yet allows us to focus on a robust setup to prove the value in Testcontainers for service-to-service testing:

Solution architecture. The client calls the cusotmer service, which is the system under test, which then calls order service.

A client calls customer-service, our service under test, which returns customer data. customer-service relies on order-service to query orders for a specific customer. A PostgreSQL database backs each of our services.

3. Testcontainers Solution

First, let’s ensure we have the appropriate dependencies, org.testcontainers core and org.testcontainers.postgresql:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.19.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.19.6</version>
    <scope>test</scope>
</dependency>

Quarkus provides us with some very important classes that we’ll leverage to write tests against our specific dependencies. From the official Quarkus guides:

A very common need is to start some services that your Quarkus application depends on before the Quarkus application starts for testing. To address this need, Quarkus provides @io.quarkus.test.common.QuarkusTestResource and io.quarkus.test.common.QuarkusTestResourceLifecycleManager.

Next, let’s declare a class that implements QuarkusTestResourceLifecycleManager and will be responsible for configuring the dependent service and its PostgreSQL database:

public class CustomerServiceTestcontainersManager implements QuarkusTestResourceLifecycleManager {

}

Now, let’s use the Testcontainers API and modules, which are preconfigured implementations of various dependencies, to wire them up for testing:

private PostgreSQLContainer<?> postgreSQLContainer;
private GenericContainer<?> orderService;

Then, we’ll configure everything we need to connect and run our dependencies:

@Override
public Map<String, String> start() {

Afterwards, we’ll need to stop our services once testing has completed:

@Override
public void stop() {
    if (orderService != null) {
        orderService.stop();
    }
    if (postgreSQLContainer != null) {
        postgreSQLContainer.stop();
    }
}

Here is where we can leverage Quarkus to manage the dependency lifecycle in our test class:

@QuarkusTestResource(CustomerServiceTestcontainersManager.class)
class CustomerResourceLiveTest {

}

Following that, we write a parameterized test that checks we’re returning a customer’s data, as well as their orders, from the dependent service:

@ParameterizedTest
@MethodSource(value = "customerDataProvider")
void givenCustomer_whenFindById_thenReturnOrders(long customerId, String customerName, int orderSize) {
    Customer response = RestAssured.given()
      .pathParam("id", customerId)
      .get()
      .thenReturn()
      .as(Customer.class);

    Assertions.assertEquals(customerId, response.id);
    Assertions.assertEquals(customerName, response.name);
    Assertions.assertEquals(orderSize, response.orders.size());
}

private static Stream<Arguments> customerDataProvider() {
    return Stream.of(Arguments.of(1, "Customer 1", 3), Arguments.of(2, "Customer 2", 1), Arguments.of(3, "Customer 3", 0));
}

Consequently, our output when running the test indicates the order service container has started:

Creating container for image: quarkus/order-service-jvm:latest
Container quarkus/order-service-jvm:latest is starting: 02ae38053012336ac577860997f74391eef3d4d5cd07cfffba5e27c66f520d9a
Container quarkus/order-service-jvm:latest started in PT1.199365S

So, we’ve successfully performed production-like testing using live dependencies, deploying what is needed to verify our service behavior end-to-end.

4. Conclusion

In this tutorial, we showcased Testcontainers as a solution for using containerized dependencies to test a Quarkus application over the wire.

Testcontainers aid us with executing reliable and repeatable tests by talking to those real services and providing a programmatic API for our test code.

As always, the source code is available over on GitHub.