Contract Testing with Quarkus and Pact

Contract Testing with Quarkus and Pact

In this article, you will learn how to create contract tests for Quarkus apps using Pact. Consumer-driven contract testing is one of the most popular strategies for verifying communication between microservices. In short, it is an approach to ensure that services can successfully communicate with each other without implementing integration tests. There are some tools especially dedicated to such a type of test. One of them is Pact. We can use this code-first tool with multiple languages including .NET, Go, Ruby, and of course, Java.

Before you start, it is worth familiarizing yourself with the Quarkus framework. There are several articles about Quarkus on my blog. If you want about to read about interesting and useful Quarkus features please refer to the post “Quarkus Tips, Tricks and Techniques” available here. For some more advanced features like testing strategies, you can read the “Advanced Testing with Quarkus” article available here.

Source Code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. Then you should just follow my instructions. Let’s begin.

Architecture

In the exercise today we will add several contract tests to the existing architecture of sample Quarkus microservices. There are three sample apps that communicate with each other through HTTP. We use Quarkus declarative REST client to call remote HTTP endpoints. The department-service app calls the endpoint exposed by the employee-service app to get a list of employees assigned to the particular department. On the other hand, the organization-service app calls endpoints exposed by department-service and employee-service.

We will implement some contract tests to verify described interactions. Each contract is signed between two sides of communication: the consumer and the provider. Pact assumes that contract code is generated and published by the consumer side, and then verified by the provider side. It provides a tool for storing and sharing contracts between consumers and providers – Pact Broker. Pact Broker exposes a simple RESTful API for publishing and retrieving contracts, and an embedded web dashboard for navigating the API. We will run it as a Docker container. However, our goal is also to run it during the CI build and then use it to exchange contracts between the tests.

Here’s the diagram that illustrates the described architecture.

quarkus-pact-arch

Running Pact Broker

Before we create any test, we will start Pact broker on the local machine. In order to do that, we need to run two containers on Docker. Pact broker requires database, so in the first step we will start the postgres container:

$ docker run -d --name postgres \
  -p 5432:5432 \
  -e POSTGRES_USER=pact \ 
  -e POSTGRES_PASSWORD=pact123 \
  -e POSTGRES_DB=pact \
  postgres

After that, we can run the container with Pact broker. We will link it to the postgres container and set the autentication credentials:

$ docker run -d --name pact-broker \
  --link postgres:postgres \
  -e PACT_BROKER_DATABASE_USERNAME=pact \
  -e PACT_BROKER_DATABASE_PASSWORD=pact123 \ 
  -e PACT_BROKER_DATABASE_HOST=postgres \
  -e PACT_BROKER_DATABASE_NAME=pact \
  -p 9292:9292 \
  pactfoundation/pact-broker

If you prefer to run everything with the single command you can use docker-compose.yml in the repository root directory. It will run not only Postgres and Pact broker, but also our three sample microservices.

version: "3.7"
services:
  postgres:
    container_name: postgres
    image: postgres
    environment:
      POSTGRES_USER: pact
      POSTGRES_PASSWORD: pact123
      POSTGRES_DB: pact
    ports:
      - "5432"
  pact-broker:
    container_name: pact-broker
    image: pactfoundation/pact-broker
    ports:
      - "9292:9292"
    depends_on:
      - postgres
    links:
      - postgres
    environment:
      PACT_BROKER_DATABASE_USERNAME: pact
      PACT_BROKER_DATABASE_PASSWORD: pact123
      PACT_BROKER_DATABASE_HOST: postgres
      PACT_BROKER_DATABASE_NAME: pact
  employee:
    image: quarkus/employee-service:1.2
    ports:
      - "8080"
  department:
    image: quarkus/department-service:1.1
    ports:
      - "8080"
    links:
      - employee
  organization:
    image: quarkus/organization-service:1.1
    ports:
      - "8080"
    links:
      - employee
      - department

Since the docker-compose.yml includes images with our sample microservices, you first need to build the Docker images of the apps. We can easily do it with Quarkus. Once we included the quarkus-container-image-jib dependency, we may build the image using Jib Maven plugin by activating the quarkus.container-image.build property as shown below. Additionally, don’t forget about skipping the tests.

$ mvn clean package -DskipTests -Dquarkus.container-image.build=true

Then just run the following command:

$ docker compose up

Finally, you can access the Pact broker UI under the http://localhost:9292 address. Of course, there are no contracts saved there, so you just see the example pact.

Create Contract Test for Consumer

Once we started a Pact broker we can proceed to the implementation of the tests. We will start from the consumer side. Both departament-service and organization-service consuming endpoints exposed by the employee-service. In the first step, we will include the Quarkus Pact Consumer extension to the Maven dependencies.

<dependency>
  <groupId>io.quarkiverse.pact</groupId>
  <artifactId>quarkus-pact-consumer</artifactId>
  <version>1.0.0.Final</version>
  <scope>provided</scope>
</dependency>

Here’s the REST client interface responsible for calling the employee-service GET /employees/department/{id} endpoint from the departament-service.

@ApplicationScoped
@Path("/employees")
@RegisterRestClient(configKey = "employee")
public interface EmployeeClient {

    @GET
    @Path("/department/{departmentId}")
    @Produces(MediaType.APPLICATION_JSON)
    List<Employee> findByDepartment(@PathParam("departmentId") Long departmentId);

}

We will test the EmployeeClient directly in the contract test. In order to implement a contract test on the consumer side we need to declare the PactConsumerTestExt JUnit5 extension. In the callFindByDepartment method, we need to prepare the expected response template as the RequestResponsePact object. The method should return an array of employees. Therefore we are using the PactDslJsonArray to construct the required object. The name of the provider is employee-service, while the name of the consumer is department-service. In order to use Pact MockServer I had to declare v3 of Pact instead of the latest v4. Then we are setting the mock server address as the RestClientBuilder base URI and test the contract.

@QuarkusTest
@ExtendWith(PactConsumerTestExt.class)
public class EmployeeClientContractTests {

    @Pact(provider = "employee-service", 
          consumer = "department-service")
    public RequestResponsePact callFindByDepartment(
        PactDslWithProvider builder) {
        DslPart body = PactDslJsonArray.arrayEachLike()
                .integerType("id")
                .stringType("name")
                .stringType("position")
                .numberType("age")
                .closeObject();
        return builder.given("findByDepartment")
                .uponReceiving("findByDepartment")
                    .path("/employees/department/1")
                    .method("GET")
                .willRespondWith()
                    .status(200)
                    .body(body).toPact();
    }

    @Test
    @PactTestFor(providerName = "employee-service", 
                 pactVersion = PactSpecVersion.V3)
    public void verifyFindDepartmentPact(MockServer mockServer) {
        EmployeeClient client = RestClientBuilder.newBuilder()
                .baseUri(URI.create(mockServer.getUrl()))
                .build(EmployeeClient.class);
        List<Employee> employees = client.findByDepartment(1L);
        assertNotNull(employees);
        assertTrue(employees.size() > 0);
        assertNotNull(employees.get(0).getId());
    }
}

The test for the integration between organization-service and department-service is pretty similar. Let’s take a look at the REST client interface.

@ApplicationScoped
@Path("/departments")
@RegisterRestClient(configKey = "department")
public interface DepartmentClient {

    @GET
    @Path("/organization/{organizationId}")
    @Produces(MediaType.APPLICATION_JSON)
    List<Department> findByOrganization(@PathParam("organizationId") Long organizationId);

    @GET
    @Path("/organization/{organizationId}/with-employees")
    @Produces(MediaType.APPLICATION_JSON)
    List<Department> findByOrganizationWithEmployees(@PathParam("organizationId") Long organizationId);

}

Here’s the implementation of our contract test. However, instead of a single endpoint, we are testing two interactions with the GET /departments/organization/{id} and GET /departments/organization/{id}/with-employees.

@QuarkusTest
@ExtendWith(PactConsumerTestExt.class)
public class DepartmentClientContractTests {

   @Pact(provider = "department-service", 
         consumer = "organization-service")
   public RequestResponsePact callFindDepartment(
      PactDslWithProvider builder) {
      DslPart body = PactDslJsonArray.arrayEachLike()
            .integerType("id")
            .stringType("name")
            .closeObject();
      DslPart body2 = PactDslJsonArray.arrayEachLike()
            .integerType("id")
            .stringType("name")
            .array("employees")
               .object()
                  .integerType("id")
                  .stringType("name")
                  .stringType("position")
                  .integerType("age")
               .closeObject()
            .closeArray();
      return builder
            .given("findByOrganization")
               .uponReceiving("findByOrganization")
                  .path("/departments/organization/1")
                  .method("GET")
               .willRespondWith()
                  .status(200)
                  .body(body)
            .given("findByOrganizationWithEmployees")
               .uponReceiving("findByOrganizationWithEmployees")
                  .path("/departments/organization/1/with-employees")
                  .method("GET")
               .willRespondWith()
                  .status(200)
                  .body(body2)
            .toPact();
   }

   @Test
   @PactTestFor(providerName = "department-service", 
                pactVersion = PactSpecVersion.V3)
   public void verifyFindByOrganizationPact(MockServer mockServer) {
      DepartmentClient client = RestClientBuilder.newBuilder()
             .baseUri(URI.create(mockServer.getUrl()))
             .build(DepartmentClient.class);
      List<Department> departments = client.findByOrganization(1L);
      assertNotNull(departments);
      assertTrue(departments.size() > 0);
      assertNotNull(departments.get(0).getId());

      departments = client.findByOrganizationWithEmployees(1L);
      assertNotNull(departments);
      assertTrue(departments.size() > 0);
      assertNotNull(departments.get(0).getId());
      assertFalse(departments.get(0).getEmployees().isEmpty());
   }

}

Publish Contracts to the Pact Broker

That’s not all. We are still on the consumer side. After running the tests we need to publish the contract to the Pact broker. It is not performed automatically by Pact. To achieve it, we first need to include the following Maven plugin:

<plugin>
  <groupId>au.com.dius.pact.provider</groupId>
  <artifactId>maven</artifactId>
  <version>4.6.0</version>
  <configuration>
    <pactBrokerUrl>http://localhost:9292</pactBrokerUrl>
  </configuration>
</plugin>

In order to publish the contract after the test we need to include the pact:publish goal to the build command as shown below.

$ mvn clean package pact:publish

Now, we can switch the Pact Broker UI. As you see, there are several pacts generated during our tests. We can recognize it by the name of the consumer and provider.

quarkus-pact-broker

We can go to the details of each contract. Here’s the description of the integration between the department-service and employee-service.

Create Contract Test for Provider

Once we published pacts to the broker, we can proceed to the implementation of contract tests on the provider side. In the current case, it is employee-service. Firstly, let’s include the Quarkus Pact Provider extension in the Maven dependencies.

<dependency>
  <groupId>io.quarkiverse.pact</groupId>
  <artifactId>quarkus-pact-provider</artifactId>
  <version>1.0.0.Final</version>
  <scope>test</scope>
</dependency>

We need to annotate the test class with the @Provider annotation and pass the name of the provider used on the consumer side (1). In the @PactBroker annotation, we have to pass the address of the broker (2). The test will load the contract published by the consumer side and test it against the running instance of the Quarkus app (under the test instance port) (3). We also need to extend the test template with the PactVerificationInvocationContextProvider class (4). Thanks to that, Pact will trigger the verification of contracts for each interaction defined by the @State method (6) (7). We also let Pact publish the verification results of each contract to the Pact broker (5).

@QuarkusTest
@Provider("employee-service") // (1)
@PactBroker(url = "http://localhost:9292") // (2)
public class EmployeeContractTests {

   @ConfigProperty(name = "quarkus.http.test-port") 
   int quarkusPort;
    
   @TestTarget
   HttpTestTarget target = new HttpTestTarget("localhost", 
      this.quarkusPort); // (3)

   @TestTemplate
   @ExtendWith(PactVerificationInvocationContextProvider.class) // (4)
   void pactVerificationTestTemplate(PactVerificationContext context) {
      context.verifyInteraction();
      System.setProperty("pact.provider.version", "1.2"); 
      System.setProperty("pact.verifier.publishResults", "true"); // (5)
   }

   @BeforeEach
   void beforeEach(PactVerificationContext context) {
      context.setTarget(new HttpTestTarget("localhost",
         this.quarkusPort));
   }

   @State("findByDepartment") // (6)
   void findByDepartment() {

   }

   @State("findByOrganization") // (7)
   void findByOrganization() {

   }
}

The value of @State refers to the name of the integration set on the consumer side. For example, line (6) in the code source above verifies the contract defined in the department-sevice as shown below.

As I mentioned before, the contract verification results are published to the Pact broker. You can check the status of verification using Pact Broker UI:

quarkus-pact-verification

Run Tests in the CI Pipeline

Our last goal is to prepare the CI process for running Pact broker and contract tests during the build of our Quarkus apps. We will use CircleCI for that. Before running the contract tests we need to run the Pact broker container using Docker Compose. In order to do that we first need to use the Linux machine as a default executor and the docker orb (1). After that, we need to install Docker Compose and then use it to run the already prepared configuration in our docker-compose.yml file (2) (3). Then we can use the maven orb to run tests and publish contracts to the instance of the broker running during the tests (4).

version: 2.1

jobs:
  analyze:
    executor: # (1)
      name: docker/machine
      image: ubuntu-2204:2022.04.2
    steps:
      - checkout
      - docker/install-docker-compose # (2)
      - maven/with_cache:
          steps:
            - run:
                name: Build Images
                command: mvn package -DskipTests -Dquarkus.container-image.build=true
      - run: # (3)
          name: Run Pact Broker
          command: docker-compose up -d
      - maven/with_cache: # (4)
          steps:
            - run:
                name: Run Tests
                command: mvn package pact:publish -Dquarkus.container-image.build=false
      - maven/with_cache:
          steps:
            - run:
                name: Sonar Analysis
                command: mvn package sonar:sonar -DskipTests -Dquarkus.container-image.build=false


orbs:
  maven: circleci/maven@1.4.1
  docker: circleci/docker@2.2.0

workflows:
  maven_test:
    jobs:
      - analyze:
          context: SonarCloud

Here’s the final result of our build.

Final Thoughts

Contract testing is a useful approach for verifying interactions between microservices. Thanks to the Quarkus Pact extensions you can easily implement contract tests for your Quarkus apps. In this article, I showed how to use a Pact broker to store and share contracts between the tests. However, you can as well use the @PactFolder options to keep the contract JSON manifests inside the Git repository.

Leave a Reply