Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Contract Testing for API Contracts! ๐ In this guide, weโll explore how to ensure your APIs behave exactly as promised, preventing those dreaded integration failures.
Youโll discover how contract testing can transform your API development experience. Whether youโre building microservices ๐๏ธ, RESTful APIs ๐, or working with third-party integrations ๐ค, understanding contract testing is essential for building reliable, maintainable systems.
By the end of this tutorial, youโll feel confident implementing contract tests that catch breaking changes before they reach production! Letโs dive in! ๐โโ๏ธ
๐ Understanding Contract Testing
๐ค What is Contract Testing?
Contract testing is like a handshake agreement between services ๐ค. Think of it as a legally binding document that ensures both the API provider and consumer stick to their promises!
In Python terms, contract testing verifies that:
- โจ API providers deliver the exact response format promised
- ๐ API consumers send requests in the expected format
- ๐ก๏ธ Changes donโt break existing integrations
๐ก Why Use Contract Testing?
Hereโs why developers love contract testing:
- Early Detection ๐: Catch breaking changes during development
- Independent Testing ๐ป: Test services without full integration setup
- Clear Documentation ๐: Contracts serve as living API docs
- Deployment Confidence ๐ง: Deploy services independently with confidence
Real-world example: Imagine an e-commerce system ๐. With contract testing, you can ensure the payment service API always returns the expected response format, preventing checkout failures!
๐ง Basic Syntax and Usage
๐ Simple Example with Pact
Letโs start with a friendly example using Pact, a popular contract testing framework:
# ๐ Hello, Contract Testing!
from pact import Consumer, Provider
# ๐จ Creating a simple contract
pact = Consumer('OrderService').has_pact_with(
Provider('PaymentService'),
host_name='localhost',
port=1234
)
# ๐ Define the expected interaction
(pact
.given('a valid payment request') # ๐ฏ Provider state
.upon_receiving('a payment request') # ๐จ Request description
.with_request('POST', '/payments', # ๐ง Request details
headers={'Content-Type': 'application/json'},
body={'amount': 99.99, 'currency': 'USD'})
.will_respond_with(200, # โ
Expected response
headers={'Content-Type': 'application/json'},
body={'status': 'approved', 'transaction_id': '12345'}))
๐ก Explanation: Weโre defining a contract between OrderService and PaymentService. The contract specifies exactly what request to expect and what response to return!
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Consumer test
import requests
from pact import Consumer, Provider
def test_payment_approval():
# ๐ฏ Set up the contract
pact = Consumer('OrderService').has_pact_with(Provider('PaymentService'))
with pact:
# ๐ Define expected interaction
pact.given('payment can be processed')
pact.upon_receiving('a payment request')
pact.with_request('POST', '/payments',
body={'amount': 50.0})
pact.will_respond_with(200,
body={'status': 'approved'})
# ๐ Make the actual request
response = requests.post(
pact.uri + '/payments',
json={'amount': 50.0}
)
# โ
Verify the response
assert response.status_code == 200
assert response.json()['status'] == 'approved'
# ๐จ Pattern 2: Provider verification
from pact import Verifier
def test_payment_provider_honors_contract():
verifier = Verifier(
provider='PaymentService',
provider_base_url='http://localhost:5000'
)
# ๐ Verify the provider meets the contract
success, logs = verifier.verify_pacts(
'./pacts/orderservice-paymentservice.json'
)
assert success == 0 # โ
All contracts satisfied!
๐ก Practical Examples
๐ Example 1: E-Commerce Order API
Letโs build a real contract testing scenario:
# ๐๏ธ Order service contract testing
import pytest
from pact import Consumer, Provider, Like, EachLike, Term
class TestOrderAPI:
def setup_method(self):
# ๐ฏ Initialize contract
self.pact = Consumer('WebStore').has_pact_with(
Provider('OrderAPI'),
port=8080
)
self.pact.start_service()
def teardown_method(self):
self.pact.stop_service()
def test_create_order(self):
# ๐ Define the contract
expected_order = {
'order_id': Term(r'\d+', '12345'), # ๐ข Regex matcher
'status': 'pending',
'items': EachLike({ # ๐ฆ Array matcher
'product_id': Like('PROD-001'),
'quantity': Like(2),
'price': Like(29.99)
}),
'total': Like(59.98),
'created_at': Term(r'\d{4}-\d{2}-\d{2}', '2024-01-15')
}
# ๐ Set up the interaction
(self.pact
.given('products exist in inventory')
.upon_receiving('a request to create order')
.with_request('POST', '/orders',
headers={'Content-Type': 'application/json'},
body={
'customer_id': 'CUST-123',
'items': [
{'product_id': 'PROD-001', 'quantity': 2}
]
})
.will_respond_with(201,
headers={'Content-Type': 'application/json'},
body=expected_order))
# ๐ฎ Test the interaction
with self.pact:
import requests
response = requests.post(
f"{self.pact.uri}/orders",
json={
'customer_id': 'CUST-123',
'items': [{'product_id': 'PROD-001', 'quantity': 2}]
},
headers={'Content-Type': 'application/json'}
)
# โ
Verify response
assert response.status_code == 201
data = response.json()
assert data['status'] == 'pending'
assert len(data['items']) > 0
print(f"โจ Order created: {data['order_id']}")
๐ฏ Try it yourself: Add contract tests for order cancellation and status updates!
๐ฎ Example 2: User Authentication API
Letโs create contracts for authentication:
# ๐ Authentication contract testing
from datetime import datetime, timedelta
import jwt
from pact import Consumer, Provider, Like, Term
class TestAuthAPI:
def setup_method(self):
# ๐ฏ Set up authentication contract
self.pact = Consumer('MobileApp').has_pact_with(
Provider('AuthService'),
port=9000
)
self.pact.start_service()
def teardown_method(self):
self.pact.stop_service()
def test_user_login(self):
# ๐ Expected token format
expected_response = {
'access_token': Term(r'^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$',
'eyJ0eXAiOiJKV1QiLCJhbGc...'), # ๐ซ JWT pattern
'token_type': 'Bearer',
'expires_in': Like(3600),
'user': {
'id': Like('user-123'),
'email': Like('[email protected]'),
'roles': EachLike('user') # ๐ฅ Array of roles
}
}
# ๐ Define login interaction
(self.pact
.given('user exists with valid credentials')
.upon_receiving('a login request')
.with_request('POST', '/auth/login',
headers={'Content-Type': 'application/json'},
body={
'email': '[email protected]',
'password': 'SecurePass123!'
})
.will_respond_with(200,
headers={'Content-Type': 'application/json'},
body=expected_response))
# ๐ Test the login flow
with self.pact:
import requests
# ๐ Attempt login
response = requests.post(
f"{self.pact.uri}/auth/login",
json={
'email': '[email protected]',
'password': 'SecurePass123!'
}
)
# โ
Verify successful login
assert response.status_code == 200
data = response.json()
assert 'access_token' in data
assert data['token_type'] == 'Bearer'
print("๐ Login successful!")
def test_token_refresh(self):
# ๐ Test token refresh contract
(self.pact
.given('a valid refresh token exists')
.upon_receiving('a token refresh request')
.with_request('POST', '/auth/refresh',
headers={
'Content-Type': 'application/json',
'Authorization': 'Bearer old.jwt.token'
})
.will_respond_with(200,
body={
'access_token': Like('new.jwt.token'),
'expires_in': Like(3600)
}))
with self.pact:
import requests
response = requests.post(
f"{self.pact.uri}/auth/refresh",
headers={'Authorization': 'Bearer old.jwt.token'}
)
assert response.status_code == 200
assert 'access_token' in response.json()
print("โจ Token refreshed successfully!")
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: State Management
When youโre ready to level up, implement provider states:
# ๐ฏ Advanced provider state handling
from flask import Flask, jsonify, request
from pact import Provider
app = Flask(__name__)
# ๐๏ธ In-memory state
users = {}
orders = {}
# ๐จ Provider state setup
@app.route('/_pact/provider_states', methods=['POST'])
def provider_states():
state = request.json['state']
if state == 'user exists with ID 123':
# ๐ค Set up user state
users['123'] = {
'id': '123',
'name': 'Test User',
'email': '[email protected]'
}
elif state == 'order exists with items':
# ๐ฆ Set up order state
orders['ORDER-001'] = {
'id': 'ORDER-001',
'user_id': '123',
'items': [
{'product': 'Widget', 'quantity': 2}
],
'status': 'pending'
}
return jsonify({'result': state})
# ๐ API endpoints that use state
@app.route('/users/<user_id>')
def get_user(user_id):
if user_id in users:
return jsonify(users[user_id])
return jsonify({'error': 'User not found'}), 404
# ๐ง Verify contracts with state
def verify_pact_with_states():
from pact import Verifier
verifier = Verifier(
provider='UserService',
provider_base_url='http://localhost:5000'
)
# ๐ฏ Point to provider states endpoint
verifier.provider_states_setup_url = 'http://localhost:5000/_pact/provider_states'
success, logs = verifier.verify_pacts(
'./pacts/consumer-userservice.json'
)
return success == 0
๐๏ธ Advanced Topic 2: Message Queue Contracts
For the brave developers working with async messaging:
# ๐ Message queue contract testing
from pact import MessageConsumer, MessageProvider, Like
class TestMessageContracts:
def test_order_placed_message(self):
# ๐จ Define message contract
pact = MessageConsumer('NotificationService').has_pact_with(
MessageProvider('OrderService'),
publish_verification_results=True
)
# ๐ฏ Expected message format
expected_message = {
'event_type': 'order.placed',
'timestamp': Term(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z',
'2024-01-15T10:30:00Z'),
'data': {
'order_id': Like('ORDER-123'),
'customer_email': Like('[email protected]'),
'total_amount': Like(99.99),
'items': EachLike({
'name': Like('Product'),
'quantity': Like(1),
'price': Like(49.99)
})
}
}
# ๐ Set up message expectation
(pact
.given('an order has been placed')
.expects_to_receive('an order placed event')
.with_content(expected_message)
.with_metadata({'routing_key': 'orders.placed'}))
# ๐ Handler that processes the message
def handle_order_placed(message):
# ๐ง Send notification email
email_data = message['data']
print(f"๐ง Sending order confirmation to {email_data['customer_email']}")
return True
# โ
Verify message handling
with pact:
# Simulate receiving the message
result = handle_order_placed(expected_message)
assert result is True
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Over-Specifying Contracts
# โ Wrong way - too rigid!
pact.will_respond_with(200, body={
'user_id': '12345', # ๐ฅ Hardcoded ID will break!
'created_at': '2024-01-15 10:30:45', # ๐ฅ Exact timestamp!
'session_id': 'abc123def456' # ๐ฅ Specific session!
})
# โ
Correct way - flexible matchers!
from pact import Like, Term
pact.will_respond_with(200, body={
'user_id': Like('12345'), # ๐ฏ Any string ID
'created_at': Term(r'\d{4}-\d{2}-\d{2}', '2024-01-15'), # ๐
Date pattern
'session_id': Term(r'[a-f0-9]{12}', 'abc123def456') # ๐ Session pattern
})
๐คฏ Pitfall 2: Ignoring Provider States
# โ Dangerous - no state setup!
def test_delete_user():
pact.upon_receiving('delete user request')
pact.with_request('DELETE', '/users/999') # ๐ฅ User might not exist!
pact.will_respond_with(204)
# โ
Safe - proper state management!
def test_delete_user():
pact.given('user exists with ID 999') # ๐ฏ Ensure user exists
pact.upon_receiving('delete user request')
pact.with_request('DELETE', '/users/999')
pact.will_respond_with(204) # โ
Safe deletion!
๐ ๏ธ Best Practices
- ๐ฏ Use Matchers Wisely: Donโt over-specify - use Like() and Term() for flexibility
- ๐ Document Provider States: Clear state names help providers implement correctly
- ๐ก๏ธ Version Your Contracts: Use pact broker for contract versioning
- ๐จ Keep Contracts Focused: One contract per consumer-provider pair
- โจ Test Happy and Sad Paths: Include error scenarios in contracts
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Weather API Contract Test
Create contract tests for a weather service API:
๐ Requirements:
- โ Get current weather by city name
- ๐ท๏ธ Support multiple temperature units (Celsius/Fahrenheit)
- ๐ค Handle city not found errors
- ๐ Include forecast endpoint with 5-day data
- ๐จ Each response needs weather emoji based on conditions!
๐ Bonus Points:
- Add provider state for different weather conditions
- Implement contract versioning
- Create both consumer and provider tests
๐ก Solution
๐ Click to see solution
# ๐ฏ Weather API contract testing solution!
from pact import Consumer, Provider, Like, EachLike, Term
import pytest
import requests
class TestWeatherAPIContract:
def setup_method(self):
# ๐ค๏ธ Set up weather service contract
self.pact = Consumer('WeatherApp').has_pact_with(
Provider('WeatherService'),
port=8888
)
self.pact.start_service()
def teardown_method(self):
self.pact.stop_service()
def test_get_current_weather(self):
# โ๏ธ Expected weather response
expected_weather = {
'city': Like('London'),
'temperature': Like(20.5),
'unit': Term(r'celsius|fahrenheit', 'celsius'),
'condition': Like('sunny'),
'emoji': Term(r'[โ๏ธ๐ค๏ธโ
๐ฆ๏ธ๐ง๏ธโ๏ธ]', 'โ๏ธ'),
'humidity': Like(65),
'wind_speed': Like(15.5),
'timestamp': Term(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z',
'2024-01-15T12:00:00Z')
}
# ๐ Define the interaction
(self.pact
.given('weather data exists for London')
.upon_receiving('a request for current weather')
.with_request('GET', '/weather/current',
query={'city': 'London', 'units': 'celsius'})
.will_respond_with(200,
headers={'Content-Type': 'application/json'},
body=expected_weather))
# ๐ Test the interaction
with self.pact:
response = requests.get(
f"{self.pact.uri}/weather/current",
params={'city': 'London', 'units': 'celsius'}
)
assert response.status_code == 200
data = response.json()
assert data['city'] == 'London'
assert 'emoji' in data
print(f"๐ก๏ธ Current weather: {data['temperature']}ยฐC {data['emoji']}")
def test_get_weather_forecast(self):
# ๐
5-day forecast contract
expected_forecast = {
'city': Like('Paris'),
'forecast': EachLike({
'date': Term(r'\d{4}-\d{2}-\d{2}', '2024-01-16'),
'high': Like(25.0),
'low': Like(15.0),
'condition': Like('partly cloudy'),
'emoji': Like('โ
'),
'precipitation_chance': Like(30)
}, minimum=5)
}
(self.pact
.given('forecast data exists for Paris')
.upon_receiving('a request for 5-day forecast')
.with_request('GET', '/weather/forecast',
query={'city': 'Paris', 'days': '5'})
.will_respond_with(200, body=expected_forecast))
with self.pact:
response = requests.get(
f"{self.pact.uri}/weather/forecast",
params={'city': 'Paris', 'days': '5'}
)
assert response.status_code == 200
data = response.json()
assert len(data['forecast']) >= 5
print("๐ 5-day forecast received!")
def test_city_not_found(self):
# โ Error scenario contract
error_response = {
'error': 'city_not_found',
'message': Like('City not found: InvalidCity'),
'suggestions': EachLike('London')
}
(self.pact
.given('city does not exist in database')
.upon_receiving('a request for invalid city')
.with_request('GET', '/weather/current',
query={'city': 'InvalidCity'})
.will_respond_with(404, body=error_response))
with self.pact:
response = requests.get(
f"{self.pact.uri}/weather/current",
params={'city': 'InvalidCity'}
)
assert response.status_code == 404
data = response.json()
assert data['error'] == 'city_not_found'
print("โ ๏ธ City not found handled correctly!")
# ๐ฎ Provider verification
def verify_weather_provider():
from pact import Verifier
verifier = Verifier(
provider='WeatherService',
provider_base_url='http://localhost:5000'
)
# ๐ฏ Add provider states
verifier.provider_states_setup_url = 'http://localhost:5000/_pact/provider_states'
success, logs = verifier.verify_pacts(
'./pacts/weatherapp-weatherservice.json',
verbose=True
)
print("โ
All weather contracts verified!" if success == 0 else "โ Contract verification failed!")
return success == 0
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create contract tests between services with confidence ๐ช
- โ Use flexible matchers to avoid brittle tests ๐ก๏ธ
- โ Implement provider states for realistic testing ๐ฏ
- โ Debug contract failures like a pro ๐
- โ Build reliable microservices with contract testing! ๐
Remember: Contract testing is your safety net for distributed systems. It catches integration issues before they become production nightmares! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered contract testing for API contracts!
Hereโs what to do next:
- ๐ป Practice with the weather API exercise above
- ๐๏ธ Add contract tests to your existing APIs
- ๐ Move on to our next tutorial: Property-Based Testing
- ๐ Set up a Pact Broker for team collaboration!
Remember: Every microservice expert started with their first contract test. Keep testing, keep learning, and most importantly, keep your APIs reliable! ๐
Happy contract testing! ๐๐โจ