An open API service indexing awesome lists of open source software.

https://github.com/openliberty/guide-contract-testing

A guide on how to implement contract testing to ensure compatibility for Java microservices.
https://github.com/openliberty/guide-contract-testing

Last synced: about 1 year ago
JSON representation

A guide on how to implement contract testing to ensure compatibility for Java microservices.

Awesome Lists containing this project

README

          

// Copyright (c) 2022, 2025 IBM Corporation and others.
// Licensed under Creative Commons Attribution-NoDerivatives
// 4.0 International (CC BY-ND 4.0)
// https://creativecommons.org/licenses/by-nd/4.0/
//
// Contributors:
// IBM Corporation
//
:projectid: contract-testing
:page-layout: guide-multipane
:page-duration: 30 minutes
:page-releasedate: 2021-01-25
:page-guide-category: microprofile
:page-essential: false
:page-description: Learn how to implement contract testing to ensure compatibility for Java microservices.
:page-seo-title: Implement consumer-driven contract testing for Java microservices using the Pact framework
:page-seo-description: A getting started tutorial with examples on how to implement consumer-driven contract testing using the Pact framework for Java microservices and cloud-native applications written using MicroProfile and Jakarta EE API.
:guide-author: Open Liberty
:page-tags: ['microprofile', 'jakarta-ee']
:page-related-guides: ['microshed-testing', 'reactive-service-testing', 'arquillian-managed']
:page-permalink: /guides/{projectid}
:repo-description: Visit the https://openliberty.io/guides/{projectid}.html[website] for the rendered version of the guide.
:common-includes: https://raw.githubusercontent.com/OpenLiberty/guides-common/prod
:source-highlighter: prettify
:imagesdir: /img/guide/{projectid}
= Testing microservices with consumer-driven contracts

[.hidden]
NOTE: This repository contains the guide documentation source. To view the guide in published form, view it on the https://openliberty.io/guides/{projectid}.html[Open Liberty website].

Learn how to test Java microservices with consumer-driven contracts in Open Liberty.

== What you'll learn

With a microservices-based architecture, you need robust testing to ensure that microservices that depend on one another are able to communicate effectively. Typically, to prevent multiple points of failure at different integration points, a combination of unit, integration, and end-to-end tests are used. While unit tests are fast, they are less trustworthy because they run in isolation and usually rely on mock data.

Integration tests address this issue by testing against real running services. However, they tend to be slow as the tests depend on other microservices and are less reliable because they are prone to external changes.

Usually, end-to-end tests are more trustworthy because they verify functionality from the perspective of a user. However, a graphical user interface (GUI) component is often required to perform end-to-end tests, and GUI components rely on third-party software, such as Selenium, which requires heavy computation time and resources.

*What is contract testing?*

Contract testing bridges the gaps among the shortcomings of these different testing methodologies. Contract testing is a technique for testing an integration point by isolating each microservice and checking whether the HTTP requests and responses that the microservice transmits conform to a shared understanding that is documented in a contract. This way, contract testing ensures that microservices can communicate with each other.

https://docs.pact.io/[Pact^] is an open source contract testing tool for testing HTTP requests, responses, and message integrations by using contract tests.

The https://docs.pact.io/pact_broker/docker_images[Pact Broker^] is an application for sharing Pact contracts and verification results. The Pact Broker is also an important piece for integrating Pact into continuous integration and continuous delivery (CI/CD) pipelines.

The two microservices you will interact with are called `system` and `inventory`. The `system` microservice returns the JVM system properties of its host. The `inventory` microservice retrieves specific properties from the `system` microservice.

You will learn how to use the Pact framework to write contract tests for the `inventory` microservice that will then be verified by the `system` microservice.

== Additional prerequisites

Before you begin, you need to install Docker if it is not already installed. For installation instructions, refer to the https://docs.docker.com/get-docker/[official Docker documentation^]. You will deploy the Pact Broker in a Docker container.

Start your Docker daemon before you proceed.

// =================================================================================================
// Getting started
// =================================================================================================
[role='command']
include::{common-includes}/gitclone.adoc[]

=== Starting the Pact Broker

Run the following command to start the Pact Broker:
[role='command']
```
docker-compose -f "pact-broker/docker-compose.yml" up -d --build
```

When the Pact Broker is running, you'll see the following output:
[role="no_copy"]
```
...
Container pact-broker-postgres-1 Started
Container pact-broker-pact-broker-1 Started
```

// Static guide instruction
ifndef::cloud-hosted[]
Go to the http://localhost:9292/[^] URL to confirm that you can access the user interface (UI) of the Pact Broker.
endif::[]

// Cloud hosted guide instruction
ifdef::cloud-hosted[]
Click the following button to visit the Pact Broker to confirm that it is working. The Pact Broker can be found at the `https://accountname-9292.theiadocker-4.proxy.cognitiveclass.ai` URL, where ***accountname*** is your account name.

::startApplication{port="9292" display="external" name="Visit Pact Broker" route="/"}

Confirm that you can access the user interface of the Pact Broker. The Pact Broker interface is similar to the following image:
endif::[]

image::pact-broker-webpage.png[Pact Broker webpage,width=95%,align="right"]

{empty} +

You can refer to the https://docs.pact.io/pact_broker/docker_images/pactfoundation[official Pact Broker documentation^] for more information about the components of the Docker Compose file.

== Implementing pact testing in the inventory service

Navigate to the `start/inventory` directory to begin.

ifdef::cloud-hosted[]
```bash
cd /home/project/guide-contract-testing/start/inventory
```
endif::[]

[role=command]
include::{common-includes}/devmode-lmp33-start.adoc[]

ifndef::cloud-hosted[]
Navigate back to the `start` directory.
endif::[]

ifdef::cloud-hosted[]
Open a new command-line session.
endif::[]

[role="code_command hotspot file=0", subs="quotes"]
----
#Create the InventoryPactIT class file.#
`inventory/src/test/java/io/openliberty/guides/inventory/InventoryPactIT.java`
----

InventoryPactIT.java
[source, java, linenums, role="code_column hide_tags=copyright"]
----
include::finish/inventory/src/test/java/io/openliberty/guides/inventory/InventoryPactIT.java[]
----

The `InventoryPactIT` class contains a [hotspot=mockprovider file=0]`PactProviderRule` mock provider that mimics the HTTP responses from the `system` microservice. The [hotspot=pact file=0]`@Pact` annotation takes the name of the microservice as a parameter, which makes it easier to differentiate microservices from each other when you have multiple applications.

The [hotspot=builder file=0]`createPactServer()` method defines the minimal expected responses for a specific endpoint, which is known as an interaction. For each interaction, the expected request and the response are registered with the mock service by using the [hotspot=verification file=0]`@PactVerification` annotation.

The test sends a real request with the [hotspot=mockTest file=0]`getUrl()` method of the mock provider. The mock provider compares the actual request with the expected request and confirms whether the comparison is successful. Finally, the [hotspot=unitTest file=0]`assertEquals()` method confirms that the response is correct.

[role="code_command hotspot file=1", subs="quotes"]
----
#Replace the inventory Maven project file.#
`inventory/pom.xml`
----

inventory/pom.xml
[source, xml, linenums, role="code_column"]
----
include::finish/inventory/pom.xml[]
----

The Pact framework provides a [hotspot=pactPlugin file=1]`Maven` plugin that can be added to the build section of the `pom.xml` file. The [hotspot=serviceProvider file=1]`serviceProvider` element defines the endpoint URL for the `system` microservice and the [hotspot=pactDirectory file=1]`pactFileDirectory` directory where you want to store the pact file. The [hotspot=pactJunit file=1]`pact-jvm-consumer-junit` dependency provides the base test class that you can use with JUnit to build unit tests.

After you create the `InventoryPactIT.java` class and replace the `pom.xml` file, Open Liberty automatically reloads its configuration.

The contract between the `inventory` and `system` microservices is known as a pact. Each pact is a collection of interactions. In this guide, those interactions are defined in the `InventoryPactIT` class.

Press the `enter/return` key to run the tests and generate the pact file from the command-line session where you started the `inventory` microservice.

When completed, you'll see a similar output to the following example:
[role="no_copy"]
```
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running io.openliberty.guides.inventory.InventoryPactIT
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.631 s - in io.openliberty.guides.inventory.InventoryPactIT
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
```

When you integrate the Pact framework in a CI/CD build pipeline, you can use the `mvn failsafe:integration-test` goal to generate the pact file. The Maven failsafe plug-in provides a lifecycle phase for running integration tests that run after unit tests. By default, it looks for classes that are suffixed with `IT`, which stands for Integration Test. You can refer to the https://maven.apache.org/surefire/maven-failsafe-plugin/[Maven failsafe plug-in documentation^] for more information.

The generated pact file is named `Inventory-System.json` and is located in the `inventory/target/pacts` directory. The pact file contains the defined interactions in JSON format:

[role="no_copy"]
----
{
...
"interactions": [
{
"description": "a request for server name",
"request": {
"method": "GET",
"path": "/system/properties/key/wlp.server.name"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": [
{
"wlp.server.name": "defaultServer"
}
]
},
"providerStates": [
{
"name": "wlp.server.name is defaultServer"
}
]
}
...
]
}
----

ifndef::cloud-hosted[]
Now, navigate to the `start/inventory` directory again.
endif::[]

ifdef::cloud-hosted[]
Open a new command-line session and navigate to the `start/inventory` directory.

```bash
cd /home/project/guide-contract-testing/start/inventory
```
endif::[]

Publish the generated pact file to the Pact Broker by running the following command:
[role='command']
```
mvn pact:publish
```

After the file is published, you'll see a similar output to the following example:
[role="no_copy"]
```
--- maven:4.1.21:publish (default-cli) @ inventory ---
Publishing 'Inventory-System.json' with tags 'open-liberty-pact' ... OK
```

== Verifying the pact in the Pact Broker

// Static guide instruction
ifndef::cloud-hosted[]
Refresh the Pact Broker webpage at the http://localhost:9292/[^] URL to verify that a new entry exists. The last verified column doesn't show a timestamp because the `system` microservice hasn't verified the pact yet.
endif::[]

// Cloud hosted guide instruction
ifdef::cloud-hosted[]
Refresh the Pact Broker at the `https://accountname-9292.theiadocker-4.proxy.cognitiveclass.ai` URL, where ***accountname*** is your account name.

::startApplication{port="9292" display="external" name="Visit Pact Broker" route="/"}

The last verified column doesn't show a timestamp because the ***system*** microservice hasn't verified the pact yet.
endif::[]

image::pact-broker-webpage-refresh.png[Pact Broker webpage for new entry,width=95%,align="right"]

{empty} +

// Static guide instruction
ifndef::cloud-hosted[]
You can see detailed insights about each interaction by going to the http://localhost:9292/pacts/provider/System/consumer/Inventory/latest[^] URL, as shown in the following image:
endif::[]

// Cloud hosted guide instruction
ifdef::cloud-hosted[]
You can see detailed insights about each interaction by clicking the following button or going to the `https://accountname-9292.theiadocker-4.proxy.cognitiveclass.ai/pacts/provider/System/consumer/Inventory/latest` URL, where ***accountname*** is your account name.

::startApplication{port="9292" display="external" name="Visit Pact Broker" route="/pacts/provider/System/consumer/Inventory/latest"}

The insights look similar to the following image:
endif::[]

image::pact-broker-interactions.png[Pact Broker webpage for Interactions,width=95%,align="right"]

== Implementing pact testing in the system service

Open another command-line session and navigate to the `start/system` directory.

ifdef::cloud-hosted[]
```bash
cd /home/project/guide-contract-testing/start/system
```
endif::[]

Start Open Liberty in dev mode for the `system` microservice:
[role=command]
```
mvn liberty:dev
```

After you see the following message, your Liberty instance is ready in dev mode:

[role="no_copy"]
----
**************************************************************
* Liberty is running in dev mode.
----
{empty} +

ifndef::cloud-hosted[]
Open another command-line session and navigate back to the `start` directory to continue.
endif::[]

ifdef::cloud-hosted[]
Open a new command-line session.
endif::[]

[role="code_command hotspot file=0", subs="quotes"]
----
#Create the SystemBrokerIT class file.#
`system/src/test/java/it/io/openliberty/guides/system/SystemBrokerIT.java`
----

SystemBrokerIT.java
[source, java, linenums, role='code_column hide_tags=copyright']
----
include::finish/system/src/test/java/it/io/openliberty/guides/system/SystemBrokerIT.java[]
----

InventoryPactIT.java
[source, java, linenums, role="code_column hide_tags=copyright"]
----
include::finish/inventory/src/test/java/io/openliberty/guides/inventory/InventoryPactIT.java[]
----

The connection information for the Pact Broker is provided with the [hotspot=connectionInfo file=0]`@PactBroker` annotation. The dependency also provides a JUnit5 Invocation Context Provider with the [hotspot=invocation file=0]`pactVerificationTestTemplate()` method to generate a test for each of the interactions.

The [hotspot=publish file=0]`pact.verifier.publishResults` property is set to `true` so that the results are sent to the Pact Broker after the tests are completed.

The test target is defined in the [hotspot=context file=0]`PactVerificationContext` context to point to the running endpoint of the `system` microservice.

The [hotspot=state file=0]`@State` annotation must match the [hotspot=given file=1]`given()` parameter that was provided in the `inventory` test class so that Pact can identify which test case to run against which endpoint.

[role="code_command hotspot file=2", subs="quotes"]
----
#Replace the system Maven project file.#
`system/pom.xml`
----

system/pom.xml
[source, xml, linenums, role='code_column hide_tags=copyright']
----
include::finish/system/pom.xml[]
----

The `system` microservice uses the [hotspot=pactDependency file=2]`junit5` pact provider dependency to connect to the Pact Broker and verify the pact file. Ideally, in a CI/CD build pipeline, the [hotspot=version file=2]`pact.provider.version` element is dynamically set to the build number so that you can identify where a breaking change is introduced.

After you create the `SystemBrokerIT.java` class and replace the `pom.xml` file, Open Liberty automatically reloads its configuration.

== Verifying the contract

In the command-line session where you started the `system` microservice, press the `enter/return` key to run the tests to verify the pact file. When you integrate the Pact framework into a CI/CD build pipeline, you can use the `mvn failsafe:integration-test` goal to verify the pact file from the Pact Broker.

The tests fail with the following errors:
[role="no_copy"]
```
[ERROR] Failures:
[ERROR] SystemBrokerIT.pactVerificationTestTemplate:28 Pact between Inventory (1.0-SNAPSHOT) and System - Upon a request for the version
Failures:

1) Verifying a pact between Inventory and System - a request for the version has a matching body

1.1) body: $.system.properties.version Expected "1.x" (String) to be a decimal number

[INFO]
[ERROR] Tests run: 4, Failures: 1, Errors: 0, Skipped: 0
```

The test from the `system` microservice fails because the `inventory` microservice was expecting a decimal, `1.1`, for the value of the `system.properties.version` property, but it received a string, `"1.1"`.

Correct the value of the [hotspot=decimal file=0]`system.properties.version` property to a decimal.
[role="code_command hotspot file=0", subs="quotes"]
----
#Replace the SystemResource class file.#
`system/src/main/java/io/openliberty/guides/system/SystemResource.java`
----

SystemResource.java
[source, java, linenums, role="code_column hide_tags=copyright"]
----
include::finish/system/src/main/java/io/openliberty/guides/system/SystemResource.java[]
----

Press the `enter/return` key to rerun the tests from the command-line session where you started the `system` microservice.

If the tests are successful, you'll see a similar output to the following example:
[role="no_copy"]
```
...
Verifying a pact between pact between Inventory (1.0-SNAPSHOT) and System

Notices:
1) The pact at http://localhost:9292/pacts/provider/System/consumer/Inventory/pact-version/XXX is being verified because it matches the following configured selection criterion: latest pact for a consumer version tagged 'open-liberty-pact'

[from Pact Broker http://localhost:9292/pacts/provider/System/consumer/Inventory/pact-version/XXX]
Given version is 1.1
a request for the version
returns a response which
has status code 200 (OK)
has a matching body (OK)
[main] INFO au.com.dius.pact.provider.DefaultVerificationReporter - Published verification result of 'au.com.dius.pact.core.pactbroker.TestResult$Ok@4d84dfe7' for consumer 'Consumer(name=Inventory)'
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.835 s - in it.io.openliberty.guides.system.SystemBrokerIT
...
```

// Static guide instruction
ifndef::cloud-hosted[]
After the tests are complete, refresh the Pact Broker webpage at the http://localhost:9292/[^] URL to confirm that the last verified column now shows a timestamp:
endif::[]

// Cloud hosted guide instruction
ifdef::cloud-hosted[]
After the tests are complete, refresh the Pact Broker at the `https://accountname-9292.theiadocker-4.proxy.cognitiveclass.ai` URL, where ***accountname*** is your account name.

::startApplication{port="9292" display="external" name="Visit Pact Broker" route="/"}

Confirm that the last verified column now shows a timestamp:
endif::[]

image::pact-broker-webpage-verified.png[Pact Broker webpage for verified,width=95%,align="right"]

{empty} +

The pact file that's created by the `inventory` microservice was successfully verified by the `system` microservice through the Pact Broker. This ensures that responses from the `system` microservice meet the expectations of the `inventory` microservice.

== Tearing down the environment

When you are done checking out the service, exit dev mode by pressing `CTRL+C` in the command-line sessions where you ran the Liberty instances for the `system` and `inventory` microservices.

Navigate back to the `/guide-contract-testing` directory and run the following commands to remove the Pact Broker:

ifdef::cloud-hosted[]
```bash
cd /home/project/guide-contract-testing
```
endif::[]

[role='command']
```
docker-compose -f "pact-broker/docker-compose.yml" down
docker rmi postgres:17.2
docker rmi pactfoundation/pact-broker:latest
docker volume rm pact-broker_postgres-volume
```

== Great work! You're done!

You implemented contract testing in Java microservices by using Pact and verified the contract with the Pact Broker.

== Related Links

Learn more about the Pact framework.

https://pact.io/[Go to the Pact website.^]

include::{common-includes}/attribution.adoc[subs="attributes"]