In a recent project, we had two frontend applications both of which are consuming common microservice APIs (all developed by our team). During the project we decided to go ahead with creating Consumer Driven Contract tests to make sure that all the applications were working well with each other.
A contract is a collection of agreements between a frontend (Consumer) and an API (Provider) that describes the interactions that can take place between them. To complete the tests we made the decision to use Pact; which enabled us to write contracts, and ensure those contracts are satisfied.
To do this, the contracts need to reside at a location which is easily accessible by both the frontend (Consumer) and API (Provider) codebase, for this we used Pact Broker, a service specifically written to publish and share contracts (pacts). The Pact Broker is a free service, hosted on a cloud-based instance (that said, it can be setup ‘on-prem’ as well).
The development workflow
To summarise our development workflow - we followed the git feature branch workflow for development. Implementing our build pipeline as code which gave us the capability to have separate builds for each feature branch. This meant we didn’t have to wait for the code to merge into the master for testing, as each branch is deployed separately (via Docker) allowing us to test our changes on each branch independently.
As a Consumer Driven Contract is a pattern that drives the development of the Provider from its Consumer’s point of view, it is test-driven development (TDD) for services, and hence should run early as a part of the unit testing phase of the build pipeline.
Verifying pacts for each feature branch with a continuous integration build became a chicken and egg scenario due to the way in which the consumer and provider feature branches existed (or not) in the source control. To elaborate on why this was a challenge, consider this:
It’s not always necessary to have a provider feature branch corresponding to each consumer branch, which makes verifying consumer expectations less straightforward. To address this in our specific project, we came up with a custom pact verification workflow for feature branch based development.
Here’s the workflow we created…
Consumer side workflow
When a new feature is requested by a consumer, the developer checks out a feature branch in the consumer codebase. The developer can then use Pact to define the expectations for the new feature as contracts, and proceed to commit and push the code.
We used semantic versioning for our application version where PATCH is the build number and hence this increases with every build. When publishing a pact, we provide the application version number so that we know which version of the consumer the pact belongs to.
The build pipeline executes the consumer side tests and publishes the new pacts to the Pact Broker. The Pact Broker then provides us the capability to tag these versioned pacts with the branch name so that our provider build pipeline can fetch the appropriately tagged pacts from the Pact Broker. In summary, the pacts are published with the application version number and tagged with the branch name.
After publishing the pacts, the build pipeline triggers the provider verification stage. In this stage we are passing the branch name as a parameter so that the pacts from the broker are fetched based on the tag == branch name criteria.
At this point, we have a feature branch for the consumer side code and defined expectations for the provider (published to the Pact Broker). We do not have a corresponding feature branch for the provider yet, as we want our tests to drive the implementation of the provider API.
The build pipeline fetches the provider codebase from the master branch and verifies the pacts published by feature branch. The Pact validation step (in the build pipeline) results in two scenarios:
To implement the feature in API, the checkout feature branch has to have the same name as the consumer feature branch. As a practice, we follow the expand and contract pattern in our API to provide backward compatibility to all the clients. Let the published pacts (tagged with the branch name) drive the expected feature implementation. Once the feature is implemented, we can commit and push the code. Build pipeline will check for published pacts from all the consumers having the tag == branch name, and will verify the expectations for all consumers.
If all the expectations are satisfied, the provider code is ready to be merged with the master. We can merge the provider code to the master and again re-trigger the consumer branch build manually. This time, when the build pipeline fetches the provider code from master branch, it will get the new feature built in and hence the feature expectations which are published to the Pact Broker will be satisfied. Once the consumer branch build is successful, the consumer code is ready to be merged to the master branch as well.
Provider side workflow
There are instances however when we need to make code changes on the API side without having the new expectation published from consumer(s). In such instances, we still need to make sure we not breaking any of the existing consumer expectations. To do this, checkout the branch from the master on the provider side, make the code changes, commit and push the code. Our build pipeline, in such instances will again first search for pacts tagged with the provider branch name only, but it will not find any pacts tagged with this branch name in the Pact Broker. It will then fetch the latest pacts for all the consumers against the master branch (tag == master).
As we always have a master branch in a Git repository, we are assured that we will get pacts for all the consumers tagged as ‘master’ and will verify the expectations for all consumers. If all the expectations are satisfied, our code changes have not broken any consumer expectation and therefore the provider code is ready to be merged to the master.
If any of the expectations fail, it indicates that the provider code changes have broken some consumer expectations. The build will fail and it will prevent us from merging broken functionality to the master.
So, hopefully this provides an overview of how we can design our build pipelines using different Git workflows to continuously test, and evolve micro-services with Pact. If you haven’t used Pact before, I’d encourage you to take a closer look at how it could help you follow a smarter Test-Driven approach to developing micro-services.
Your can find out more about Pact in our other blogs below - or visit our contract testing resources page.