Consumer-Driven Contracts with Pact

Andrew Nicholson
ITHAKA Tech
Published in
4 min readJun 6, 2018

With over 300 microservices managed by more than a dozen teams working on JSTOR, we are always looking for ways to evolve and maintain our services safely and efficiently. We have historically relied on integration tests to verify our service interactions, but this has a number of drawbacks. One of our biggest wins this past year has been starting to use Pact to replace some of our integration tests with contract tests.

Pact refers to multiple concepts: it’s a suite of tools designed to facilitate consumer-driven contracts; it’s also the term for the contract document between a consumer and provider. A Pact contract is composed of example interactions, written from the perspective of the consumer’s use cases. A simple interaction might be something like: given a GET request to /books/1, then a 200 response is returned with the appropriate data. The contract is generated from unit tests exercising the client code itself, by using Pact DSLs to describe each interaction.

JS Pact DSL

The DSL provides a simple way to describe the expected interaction. These tests verify that the consumer produces the expected request (run against a mock service that responds with the expected response) and that the consumer handles the provider’s response correctly. If the tests pass, a JSON file is produced containing the interactions making up that pact, which can then be run against providers to verify their responses.

In order to integrate pacts into our development workflow, we upload them to the “pact-broker” tool provided by the folks at Pact. The pact-broker generates a nice UI for reading pacts, and provides APIs to allow provider verification tools to download pacts and upload verification results.

A simple pact in the pact-broker

Recently, one of our teams wanted to remove some deprecated fields from a JSON API and needed to know which consumers would be affected by this change. This interaction was not covered with pacts, so the team had to analyze access logs in order to discover the consumers of that API. However, while these consumers all did use this API, they did not all care about the fields in question. In order to understand that, owners of the consumers had to spend several days digging through client code in an attempt to find and document semantically significant usage. This process was time-consuming and error-prone, and relying on integration tests for verification has limits. Since these tests cross application boundaries and depend on the data state in our test environment, they can be flaky, and it can be difficult to ensure all of the consumer use cases are covered. Additionally, the usage documents produced are instantly out of date the moment any change is made on the consumer or provider side.

With pacts in-place, evolving an API is a much easier process. The pacts provide clear documentation not only for which consumers use a given endpoint, but also which fields they care about. It is easy to discover who will be affected by the removal of a particular field. As this documentation is produced by the real consumer code, and verified against the real provider code, it is kept automatically up to date with the code.

Pact network graph, seen in pact-broker

We can deploy a new version of a provider in the test environment and see if any consumers would be affected by running the verification tests and see which pacts have been broken by that change. Since this is done via contract tests in isolation, it can be done more often than integration tests.

Best of all, the effort to create and maintain pacts is significantly lower than integration tests. Creating pacts requires that you define your consumer use cases, which is something that you have to do already to some degree. By codifying those use cases in the form of Pact tests, you get documentation and automated verification more or less for free.

Pact has become a standard part of our workflow when developing new service interactions. Being able to have contracts that are kept up to date with the code, while simultaneously being able to verify those contracts automatically and in isolation, is a huge benefit. Pact libraries exist for a number of languages including Java, Python, Javascript, and Ruby. The next time you’re struggling to maintain end-to-end tests, or wondering how to make evolving microservices easier, consider adopting Pact for your service contracts.

--

--