Consumer Driven Contracts with Jackal
Jackal saves you from outages by stopping you releasing breaking changes to your APIs!
Jackal started life as a discussion around a bug introduced by changing the API of one of the Microservices without checking its consumers would handle the new API successfully. Hint: they didn’t.
It has ultimately evolved to provide a standalone Microservice and command-line client to manage contract testing in a way which can be easily automated using a CI platform.
Install from Jackal on npm.
One approach to the above problem would have been to have a suite of integration API tests separate from any service which tested the services worked together by calling out to multiple interdependent services in sequence.
This approach is similar to the current pipeline for our monolith, and presents a few, often quite frustrating problems:
- Changes must be made in two repositories when updating or extending any public API
- Changes must be made in quick enough succession to prevent a code change building successfully without sufficient tests, or failing due to obsolete tests which haven’t been removed
- Changes run through the pipeline in a branch must run against the same branch of the test project, or potentially fail
Consumer Driven Contracts
An alternative approach to this problem is to use Consumer Driven Contracts (CDC). A
Contract specifies the interaction which can occur between a
Consumer and a
Provider and aims to ensure development of a
Provider is driven by the requirements of its
The advantages this provided over a suite of integration tests are:
- Requirements are defined and maintained by the Consumer of a service, rather than by the service itself
- Contracts are more explicit as well as smaller and faster to add to an existing repository than adding a suite of integration tests in a separate location
- Contract testing provided the ability to prevent both the Consumer of the service and the service itself from releasing broken code
At FindMyPast we are currently in the process of splitting a monolithic web application into a platform of Microservices which requires a lot of API interaction. It was inevitable that a breaking change in the service communication was going to be introduced.
Each of our new Microservices presents an API to be used by other services. In order to ensure each API is tested, each service has a suite of API tests which live in the same repository as the code, this encourages RDD approach. The API test suite for each service validates the code against the business rules laid out when migrating monolith functionality to a new Microservice.
The problem this leaves, and the problem Jackal tries to solve, is to provide a guarantee of compatibility between a Microservice and its consumers.
In an attempt to ensure contracts using Jackal are idempotent and repeatable, contracts can be defined with multiple before and after hooks to carry out set up and tear down actions. Jackal can also automatically insert random data into request bodies in order to meet requirements around unique data constraints, for example, a new registration request must specify a unique email.
Our envisaged development flow for Jackal is as follows: after a contact is sent, Jackal tests the provider’s API. If the contract tests pass and the response from the provider matches the contract, successful response is propagated all the way back.
If a consumer makes a breaking change to the contract, the provider no longer matches the shape and jackal responds with a failure to the consumer side. This can be used to break the test build.
If a provider makes a breaking change to its own API, you can run the tests already saved from the previous successful consumer run. The breaking changes would be detected, returned back and used to break the build.
In order to introduce changes, while having strict consumer contract testing process in place, the provider is required to make a non-breaking change. This could either be an introduction of a new API version or an additive change to an existing response.
Jackal is available from NPM and should be installed globally in order to use the client as a normal CLI utility:
$ npm install --global jackal
The Jackal Server provides five API routes which act as an interface to the service, they do not provide a RESTful API and attempts to use them in such a fashion may cause your Jackal to be mortally wounded.
The Jackal Server uses a single configuration file in either JSON or YAML format and can be started using the Jackal Client:
$ jackal start -c path/to/config.yaml
The configuration file specifies the database file location, any additional metadata fields for logging and the server to send StatsD data to should you be so inclined. The default configuration file provides the following configuration:
db: path: db.json logger: environment: development statsD: host: localhost port: 8125 prefix: jackal
If the default configuration is ok for your needs then you can start a Jackal Server using the even simpler command:
$ jackal start
The Jackal Client can actually be thought of as two distinct parts:
- a CLI enabling usage of Jackal in a command-line environment
- a programmatic client capable of making requests to and handling responses from a Jackal Server
Early versions of Jackal only provided a server, requiring contracts to be sent using Curl or similar to send contracts to a running instance:
$ curl -X POST --silent http://jackal.url:25863/api/contracts -H 'Content-Type: application/json' -d @path/to/contracts.json
In addition to the fairly lengthy command needed to send the contracts, parsing the JSON response and determining whether contracts had passed was also left to potential users and their
By comparison, the same action can be achieved using the Jackal Client with the relatively simple:
$ jackal send http://jackal.url:25863 path/to/contracts.json
The response from the Jackal server is also handled and can be presented in one of several formats, including
teamcity formats, before an appropriate exit code is returned.
The Jackal Client also allows for other quality of life improvements:
- Contract files can be specified in either JSON or YAML format
- Contracts can be split into per Provider files under a common directory for a given Consumer
- Contracts in a directory can be skipped by simply adding a
.skippedextension to the file
- For convenience jackal can be sucessfully ran against a non-existsing contract file with
--skip-missing-contractflag, this can be used for introducing jackal in multiple builds at once with only select builds opting into the tests by actual defining the contracts file
Defining a contract
The following is a YAML example contract for an iTunes Search App:
itunes_search_app: itunes: search_by_term_and_country: OK: request: baseUrl: "https://itunes.apple.com" method: GET path: /search query: "?term=mclusky&country=gb" response: body: resultCount: Joi.number().integer() results: - collectionName: Joi.string() trackName: Joi.string() statusCode: 200
This contract can also be used in its equivalent JSON format.
Submitting the contract
Submit the contract to your running Jackal server by running the following:
$ jackal send http://localhost:25863 path/to/example/contract.yaml itunes contracts executed itunes_search_app contracts executed against itunes ✔ Test search_by_term_and_country OK passed for itunes_search_app against itunes
Run the contract for your provider
So it might be hard to convince Apple to run your contract before deploying the iTunes API server, but in a perfect world they’d be a good provider and run the following:
$ jackal run http://localhost:25863 itunes itunes contracts executed itunes_search_app contracts executed against itunes ✔ Test search_by_term_and_country OK passed for itunes_search_app against itunes
itunes refers to the provider name specified in the contract.
Full documentation for Jackal, including an extensive and (hopefully) exhaustive usage guide, can be found in our GitHub repository.