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.
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.
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:
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