Contract Tests - APIs Guided by Consumers
Integrating APIs is an activity that requires careful analysis, as it involves the dependency of a microservice on an external resource. This integration adds a level of risk to operations, as discrepancies between expected and received data can result in integration failures. Ensuring that the API integration is working correctly is a crucial priority, considering that any eventual problem can impact the end user.
An approach that generates value in scenarios involving API integration is the Contract Test. These types of tests verify, through requests, whether the calls to an external service align with the expected data. They help prevent failures by using assertions that allow for early feedback on potential problems caused by a code change that breaks the contract.
In this article, we’ll demonstrate a use case of contract tests in practice with three integrated microservices. The text has three divisions, as demonstrated below:
- Development of three integrated microservices.
- Adding contract tests to verify the integration between the microservices.
- Simulating some breaking changes to demonstrate the failure of the contract tests.
The project code is available in the pact-contract-test GitHub repository.
Book Sales System
For this article, we’ll use an example system with three microservices: person-data-service
, book-data-service
, and book-sales-service
.
The microservice book-sales-service
is responsible for performing book sales operations. To make a sale, the service depends on person-data-service
to retrieve person data and book-data-service
to retrieve book data and update the stock.

Services of book sales system.
Person Data Service
The person-data-service
has an API that receives the person code and returns the corresponding data. A request to retrieve the person data can be made as demonstrated in the curl
command below.
curl --location --request \
GET 'http://localhost:8082/persons/1001'
The request result displays the person data as demonstrated below.
{
"id" : 35,
"name" : "John",
"country" : "Brazil",
"passportNumber" : FT875654
}
The service has a simple structure composed of Controller
, Service
, and Repository
. The purpose of this exercise is to test the contract between the APIs without integrating a database. The data is persisted in an in-memory collection. Below is a diagram illustrating the main components.

Components of person-data-service
Book Data Service
The book-data-service
consists of two APIs: one that receives the book code and returns the corresponding data, and another API responsible for updating the stock. The code is structured similarly to the person-data-service
, with Controllers
, Services
, and Repositories
, as illustrated in the image below.

Components of book-data-service
.
Get book data
The curl
command below demonstrates how to make a request to retrieve book data.
curl --location --request GET 'http://localhost:8081/books/201'
The response is in JSON
format, as demonstrated below.
{
"id": 202,
"name": "Refactoring: Improving the Design of Existing Code",
"stock": 0,
"isbn": "0201485672"
}
Book stock update
The book stock update is performed through a POST
request, as demonstrated below.
curl --location 'http://localhost:8081/books/updateStock' \
--header 'Content-Type: application/json' \
--data '{"id": 202,"quantity": 1}'
The API will return the status of the operation as either SUCCESS
or FAILURE
, depending on the result. In the case of success:
{
"status": "SUCCESS",
"message":"The stock of the book 202 was update to 2."
}
In case of an issue, an error will be returned, as demonstrated below, such as in a scenario with insufficient stock.
{
"status": "FAILURE",
"message": "Failed to update stock of book 202 because the new stock will be less than 0."
}
Book Sales Service
The primary service is book-sales-service
. This service is responsible for conducting sales operations and orchestrating the integration between the other microservices.
To make a sale, it’s necessary to send a request with the book ID and person ID. The service will then retrieve the person data from person-data-service
and the book data from book-data-service
. At the end of the operation, a new request is made to book-data-service
to update the stock. This operation can be performed using the curl
command below.
curl -X POST --location 'http://localhost:8080/book-sales' \
--header 'Content-Type: application/json' \
--data '{"personId": 1001,"bookId": 203}'
If the sale is successful, a message will be returned with the operation results, as demonstrated below.
{
"status": "SUCCESS",
"message": "The sale to person John with passport number FT8966563 was successful."
}
For any sales issues, a message with more details will be returned. In the example below, a book sale is attempted when there is no stock available.
{
"status": "OUT_OF_STOCK",
"message": "The current stock of book 201 is 0 and is not sufficient to make the sale."
}
In addition to Controller
, Service
, and Repository
, this service includes components responsible for integrating with the providers: BookDataServiceWebClient
and PersonDataServiceWebClient
. The diagram below illustrates how these components are organized.

Componentes of book-sales-service
.
Contract Test
The book-sales-service
depends on both the book-data-service
and person-data-service
to conduct sales operations. Therefore, it’s essential to ensure communication between these services.
Contract Testing is an efficient way to ensure integration, avoiding issues stemming from contract breaches. Pact is a tool that has the necessary features for working with contract tests, offering functionalities that enable consumer-driven testing, meaning consumers define which contract should be followed.
Pact
Pact enables testing the integration between providers and data consumers. It offers a straightforward and practical approach, consisting of three main components: Broker
, Provider
, and Consumer
.
The broker
centralizes the contracts and serves as the platform where Consumers
publish their contracts. Providers
also use the broker
, but their aim is to validate whether their APIs meet the contracts defined by consumers.
The diagram below, provided by Pact documentation, illustrates in more detail the steps involved in the testing process.

Source: https://docs.pact.io/
Testing Contracts with Pact
The microservice responsible for consuming the APIs needs to create and publish the contract. In this scenario, the book-sales-service
acts as the Consumer
. The microservices book-data-service
and person-data-service
, responsible for providing the data, are the providers.
Consumers
The initial step involves setting up the service to utilize Pact
. In this example, Gradle
is used, requiring the addition of the plugin and dependencies to the build.gradle
file, as demonstrated below.
plugins {
[...]
id 'au.com.dius.pact' version "4.3.15"
}
dependencies {
[...]
testImplementation 'au.com.dius.pact.consumer:junit5:4.3.15'
}
It’s necessary to include the broker URL
, as demonstrated below.
pact {
publish {
pactBrokerUrl = "http://localhost:9292"
}
}
Contract with person-data-service
To create the contract with person-data-service
, it’s necessary to create a new class that extends PactConsumerTestExt
.
@ExtendWith(PactConsumerTestExt.class)
public class BookSalesConsumerForPersonDataContractTest
A new method with the contract details needs to be created, where the consumer’s requirements are defined. As demonstrated below, person-data-service
needs to return the ID, name, and passport number.
@Pact(consumer = CONSUMER_BOOK_SALES_SERVICE_GET_PERSONS, provider = PROVIDER_PERSON_DATA_SERVICE)
public V4Pact whenRequestPersonById_thenReturnsPerson(PactDslWithProvider builder) {
PactDslJsonBody bodyResponse = new PactDslJsonBody()
.integerType("id", A_PERSON_ID)
.stringType("name", A_PERSON_NAME)
.stringType("passportNumber", A_PERSON_PASSPORT_NUMBER);
return builder
.given("it has a person and status code is 200")
.uponReceiving("a request to retrieve a person by id")
.path("/persons/" +A_PERSON_ID)
.method("GET")
.willRespondWith()
.headers(Collections.singletonMap("Content-Type", "application/json"))
.status(OK.value())
.body(bodyResponse)
.toPact(V4Pact.class);
}
It’s also necessary to include a second method to verify the test. MockServer
with WebClient
is used for this purpose, as demonstrated below.
@PactTestFor(providerName = PROVIDER_PERSON_DATA_SERVICE,
pactMethod = "whenRequestPersonById_thenReturnsPerson",
providerType = SYNCH)
@Test
public void whenRequestPersonById_thenReturnsPerson(MockServer mockServer) {
// given
WebClient webClient = WebClient.builder()
.baseUrl(mockServer.getUrl())
.build();
// when
PersonDataServiceWebClient personDataServiceWebClient =
new PersonDataServiceWebClient(webClient, "/persons/{personId}");
PersonDataResponse personDataResponse = personDataServiceWebClient.retrievePerson(A_PERSON_ID);
// then
assertThat(personDataResponse.getId()).isInstanceOf(Long.class).isEqualTo(A_PERSON_ID);
assertThat(personDataResponse.getName()).isInstanceOf(String.class).isEqualTo(A_PERSON_NAME);
assertThat(personDataResponse.getPassportNumber()).isInstanceOf(String.class).isEqualTo(A_PERSON_PASSPORT_NUMBER);
}
Contract with book-data-service
In the book-data-service
, there are two dependencies: one to retrieve the book data and another to request the stock update.
@Pact(consumer = CONSUMER_BOOK_SALES_SERVICE_GET_BOOKS, provider = PROVIDER_BOOK_DATA_SERVICE)
public V4Pact whenRequestBookById_thenReturnsBook(PactDslWithProvider builder) {
PactDslJsonBody bodyResponse = new PactDslJsonBody()
.integerType("id", A_BOOK_ID)
.integerType("stock", A_BOOK_STOCK);
return builder
.given("it has a book and status code is 200")
.uponReceiving("a request to retrieve a book by id")
.path("/books/" + A_BOOK_ID)
.method("GET")
.willRespondWith()
.headers(Collections.singletonMap("Content-Type", "application/json"))
.status(OK.value())
.body(bodyResponse)
.toPact(V4Pact.class);
}
@Pact(consumer = CONSUMER_BOOK_SALES_SERVICE_UPDATE_STOCK, provider = PROVIDER_BOOK_DATA_SERVICE)
public V4Pact whenUpdateBookStock_thenReturnsStatus(PactDslWithProvider builder) {
PactDslJsonBody responseBody = new PactDslJsonBody()
.stringType("status", "SUCCESS")
.stringType("message", "The stock of the book " + A_BOOK_ID + " was update to 16");
PactDslJsonBody requestBody = new PactDslJsonBody()
.integerType("id", A_BOOK_ID)
.integerType("quantity", A_BOOK_QUANTITY)
.asBody();
return builder
.given("it has a book and stock can be updated")
.uponReceiving("a request to update the stock of book")
.method(HttpMethod.POST.name())
.path(PATH_UPDATE_STOCK)
.headers(Collections.singletonMap("Content-Type", "application/json"))
.body(requestBody)
.willRespondWith()
.headers(Map.of("Content-type", "application/json"))
.status(OK.value())
.body(responseBody)
.toPact(V4Pact.class);
}
Providers
With the contracts defined by the Consumers
, the next step is to prepare the verification for the providers. The providers are book-data-service
and person-data-service
, requiring the creation of specific tests for each of them. In this case, the inclusion of Pact
depends on the dependency and the configuration in the build.gradle
file, as demonstrated below.
dependencies {
[...]
testImplementation 'au.com.dius.pact.provider:junit5spring:4.5.6'
}
Some variables need to be defined for the test execution. They are: the url
, the version, and if the results need to be published with the test execution.
tasks.named('test') {
useJUnitPlatform()
systemProperties["pactbroker.url"] = "http://localhost:9292"
systemProperties["pact.provider.version"] = version
systemProperties["pact.verifier.publishResults"] = "true"
}
Contract verification by person-data-service
To avoid the need to start the entire application, @WebMvcTest
is used. A mock
of the service class is added so that the test context is specific to the Controller
.
@WebMvcTest
@Provider("person-data-service")
@PactBroker
public class PersonDataProviderForBookSalesContractTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private PersonService personService;
@BeforeEach
public void setUp(PactVerificationContext context){
context.setTarget(new MockMvcTestTarget(mockMvc));
}
//[...]
}
The mock
must be included in the method annotated with @State
, representing the part of the code where it connects with the Consumer
. In the @State
, it’s important that the string defined matches the one defined in the consumer
. In the case of a person, it’s included in the V4Pact
builder as given("it has a person and status code is 200")
.
@State("it has a person and status code is 200")
public void itHasPersonWithIdAndStatusIs200() {
when(personService.getPersonById(A_PERSON_ID))
.thenReturn(Person.builder()
.id(A_PERSON_ID)
.name(A_PERSON_NAME)
.passportNumber(A_PERSON_PASSPORT_NUMBER)
.build());
}
It’s necessary to add the code responsible for verifying the contract.
@TestTemplate
@ExtendWith(PactVerificationSpringProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
Contract verification by book-data-service
The same must be done for book-data-service
, the difference in this case there are two contracts: one responsible for retrieving book data and another responsible for updating the stock. Therefore, two @State
annotations are required, as demonstrated below.
@State("it has a book and status code is 200")
public void itHasBookWithIdAndStatusIs200() {
when(bookService.getBookById(A_BOOK_ID))
.thenReturn(Book.builder()
.id(A_BOOK_ID)
.stock(A_BOOK_STOCK)
.build());
}
@State("it has a book and stock can be updated")
public void aRequestToUpdateTheStockOfBook() {
when(bookService.updateStock(A_BOOK_ID, A_BOOK_QUANTITY_UPDATE_STOCK))
.thenReturn(UpdateStockResult.builder()
.message("The stock of the book " + A_BOOK_ID + " was update to 16")
.status(SUCCESS)
.build());
}
Contract Publishing and Verification
With the tests created, the next step is to publish the contract to the Pact Broker, allowing the providers to perform verification. For this, we need an instance of the broker
, and we are using a container for this purpose. Below is the code responsible for starting a container, which is part of the docker-compose
used in other services.
services:
pactbroker:
image: pactfoundation/pact-broker:2.104.0.0
environment:
PACT_BROKER_DATABASE_ADAPTER: sqlite
PACT_BROKER_DATABASE_NAME: pactbroker
ports:
- "9292:9292"
[...]
To start only the Pact Broker, run the following command. The broker will be available at http://localhost:9292/
.
docker-compose up pactbroker -d
A page like the one demonstrated below will be displayed.

Start page of pact broker.
Contract publication by the consumer
With the broker
working, the next step is to publish the contract by the consumer, the book-sales-service
. To do this, navigate to the project folder and execute the build
command, as demonstrated below.
./gradlew clean build
Some JSON
files will be generated in the build\pacts
folder. A separate file is generated for each contract, as demonstrated below.

JSON
files for the contracts.
Now it’s necessary to publish the contract to the broker
, as demonstrated below.
./gradlew pactPublish
When accessing the Contracts published by consumer.broker
, it will be possible to visualize the three published contracts.
Contract verification by the providers
With the contracts published, the next step is to verify them by the provider. During this step, it’s possible to block a release if any changes break the contract.
There are two services that need to verify the contracts. Let’s begin with person-data-service
. In the project, you need to run clean
and build
, and then the tests will be executed.
./gradlew clean build
If everything works correctly, then the verification will display the last verified columns, as demonstrated in the image below.

Contract verification by person-data-service
.
The same process must be performed for book-data-service
. In the project folder, you need to run clean
and build
.
./gradlew clean build
If the tests pass, the broker
will be updated with the performed verification.

Contract verification by book-data-service
.
Simulating Contract Breakage
A major benefit of using contract testing is the ability to ensure that providers can make changes without negatively affecting consumers. To test this scenario, let’s simulate some changes and observe how Pact
responds to this type of situation.
Simulation 1: Provider removes field from API
In the first scenario, the person-data-service
makes a change by removing the passport number field, which is essential for the book-sales-service
. With the removal of the field, the service response looks as follows.
{
"id":1002,
"name":"Maria",
"country":"Brazil"
}
This change simulation is available on a branch, to access it simply perform a checkout
.
git checkout simulation-1-person-data-remove-passport-number
With the change made, the next step is to run the contract tests for the person-data-service
. For that, the command below can be used.
./gradlew clean build
The test will fail, and the reason can be viewed in the generated report.
Report with failure details.
The Contract matrix verification.broker
also indicates a contract break in the matrix verifications.
Further details are available by clicking on the item in the matrix with failures.
Details about failure verifications.
Simulation 2: Provider renames API field
In the second simulation, the book-data-service
changes the field name from stock
to currentStock
. Below is the new response body returned by the API.
{
"id":201,
"name":"Domain-Driven Design: Tackling Complexity in the Heart of Software",
"currentStock":0,
"isbn":"9780321125217"
}
This simulation change is also available in a branch.
git checkout simulation-2-book-data-change-stock-field-name
To observe the test failure, it’s necessary to run the project build.
./gradlew clean build
A report with the failure is also generated, providing more details about the contract break.
Report with failure details.
The matrix verification displays more details about the verifications.
Contract matrix verification.
Further details can be accessed by clicking on the item in the matrix with failures.
Details about failure verifications.
Simulation 3: Provider renames field in Post API
In the third simulation, the quantity
field is renamed to quantityToUpdate
in the request to update the stock in the book-data-service
microservice. Below are more details about the new request body.
{
"id": 202,
"quantityToUpdate": 1
}
This change simulation is available on a branch.
git checkout simulation-3-book-data-rename-field-name-post-request
To observe more details about the contract break, it’s necessary to build the project.
./gradlew clean build
The report with the failures will be generated, showing more details about the contract break. In this case, success is expected; however, a status code 400 is returned, indicating that the Report with failure details.quantityToUpdate
field was not sent in the request body.
The matrix verification displays more details about the verifications.
Contract matrix verification.
Further details can be accessed by clicking on the item in the matrix with failures.
Details about failure verifications.
Conclusion
Using contract tests in an environment with multiple microservices simplifies the prevention of contract breaks that can impact API integrations. Although it requires additional effort to prepare the environment and create the contracts, once this is done, the services will be safer and less vulnerable to failures. Below are some benefits of adopting contracts in a development flow.
- Contracts make services less vulnerable to failures, as changes that affect other microservices break the contract.
- Providers with multiple consumers make each contract explicit, making it easier to understand which services depend on something specific and plan changes more safely.
- Refactorings are safer because it’s clear exactly what each consumer uses in the API. For example, fields included without a reason and nobody knows why they are present in the API.
- It’s a way to create a standard with contracts between APIs in a scenario with many teams.
- Documentation is important but vulnerable to failures. Contract tests are an automated approach that can be configured to break the build if there are any failures.
- It’s an approach similar to Test-Driven Development (TDD), where the code created in the providers are guided by contract consumer tests.
References
- Diagrams created using the C4 Model. More details at https://c4model.com/.
- Pact Documentation. https://docs.pact.io/.
- Contract Test. Martin Fowler. https://martinfowler.com/bliki/ContractTest.html.
- Consumer-Driven Contracts: A Service Evolution Pattern. Ian Robinson. https://martinfowler.com/articles/consumerDrivenContracts.html.
- Testing the Web Layer. https://spring.io/guides/gs/testing-web.
- Pact Spring/JUnit5 Support. https://docs.pact.io/implementation_guides/jvm/provider/junit5spring.