Contract Testing Demystified
A Practical Guide for Streamlined Microservice Integration and Deployment
Purpose
This document provides a comprehensive guide for implementing contract testing within our microservices architecture. It outlines the responsibilities of each engineering team, provides concrete examples of contract tests, and details the stages at which these tests should be executed. It also addresses the process for adding new features or APIs.
Overview
Contract testing ensures that services can communicate correctly by validating that the interactions between services adhere to a predefined agreement (contract). This practice allows services to be developed and released independently without breaking the production system.
If you don’t know what a contract testing is yet, or you did not understand the importance of contract testing in the world of microservice development life cycle, please refer to this 6 mins Youtube video. Assume we have already passed that question stage, and we wanted to define an actionable and practical engineering excellence plan, please read on next. Also since the goal of this article is to lay out an actionable and practical plan, I here use concrete toolset named Pact (an open source contract testing software) to describe concrete action items both at code level (imagine you are the developer) and engineering workflow level (imagine you are the engineering managers or the owner of a company’s engineering excellence initiative)
A ‘Hello World’ MicroService Architecture
Provider Service is a microservice that Consumer Service 1 and Consumer Service 2 will be relying on.
Who should write what?
Consumer Service Teams (Consumer Service 1 team and Consumer Service 2 team)
Write Consumer Contracts: Define and maintain the contracts that specify what your service expects from the provider service.
Run Consumer Contract Tests: Validate that your service can correctly send requests and handle responses according to the contracts.
Publish Contracts: Publish the consumer contracts to the Pact Broker.
Provider Service Team
Fetch Consumer Contracts: Retrieve the latest consumer contracts from the Pact Broker.
Run Provider Contract Tests: Ensure the provider service meets the specifications defined in the consumer contracts.
Validate New Features: Implement and test new features without breaking existing contracts.
Show me the Code
Consumer Service 1 (consumer-service-1/
)
consumer.py:
import requests
def get_new_feature():
response = requests.get('http://localhost:5000/new-feature')
return response.json()
tests/test_contract.py:
import unittest
from pact import Consumer, Provider
from consumer import get_new_feature
class ConsumerContractTest(unittest.TestCase):
def setUp(self):
self.pact =
Consumer('ConsumerService1').
has_pact_with(Provider('ProviderService'), port=5000)
self.pact.start_service()
def tearDown(self):
self.pact.stop_service()
def test_get_new_feature(self):
expected = {
"message": "This is the new feature",
"data": {"key": "value"}
}
(self.pact
.given('new feature is available')
.upon_receiving('a request for the new feature')
.with_request(method='GET', path='/new-feature')
.will_respond_with(200, body=expected))
with self.pact:
result = get_new_feature()
self.assertEqual(result, expected)
if __name__ == '__main__':
unittest.main()
Provider Service (provider-service/
)
app.py:
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route('/new-feature', methods=['GET'])
def new_feature():
response = {
"message": "This is the new feature",
"data": {"key": "value"}
}
return jsonify(response)
if __name__ == '__main__':
app.run(port=5000)
tests/test_contract.py:
import unittest
from pact import Verifier
class ProviderContractTest(unittest.TestCase):
def test_provider_contract(self):
verifier = Verifier(provider='ProviderService',
provider_base_url='http://localhost:5000')
pact_broker_url = 'http://localhost:9292'
# URL of the Pact Broker where consumer contracts are stored
output, logs = verifier
.verify_with_broker(
pact_broker_url=pact_broker_url,
broker_username='',
broker_password='',
publish_version='1.0.0')
self.assertTrue(output)
if __name__ == '__main__':
unittest.main()
Here I just write one consumer’s repo as illustration, the other consumer would have very similar code structure.
How the Developer Experience and CI / CD Pipeline look like?
Consumer Development Stage:
Run consumer contract tests locally before committing any changes.
Optionally, use pre-commit hooks to automate this process.
Consumer CI Pipeline:
Run consumer contract tests on every pull request to validate proposed changes.
Publish contracts to the Pact Broker upon successful PR validation.
Provider Development Stage:
Fetch the latest consumer contracts and run provider contract tests locally before committing changes.
Provider CI Pipeline:
Fetch the latest contracts from the Pact Broker at the beginning of the CI pipeline.
Run provider contract tests to ensure compliance.
If all tests pass, proceed with the build and deployment process.
What is Needed When Adding New Features / APIs
Design and Proposal:
The Provider Service team designs the new API and drafts a contract proposal.
Share the proposal with the Consumer Service 1 and Consumer Service 2 teams for review and feedback.
Update Contracts:
The Consumer Service 1 and Consumer Service 2 teams review and provide feedback on the contract.
Update or create new consumer contracts based on the new API.
Run Tests:
Consumers: The Consumer Service 1 and Consumer Service 2 teams run consumer contract tests to validate their services with the new API.
Provider: The Provider Service team runs provider contract tests to ensure the new feature complies with the updated contracts.
CI/CD Pipeline Integration:
Consumers publish the updated contracts to the Pact Broker.
The Provider Service team fetches and verifies these contracts in the CI pipeline.
If all tests pass, deploy the new feature.
Conclusion
By following this design document, the engineering teams will be able to implement contract testing effectively. This will ensure seamless communication between services, allow independent releases, and maintain a stable production environment.