In modern software development, applications often rely on external APIs or microservices to function. Ensuring that these integrations work correctly while maintaining flexibility requires contract testing. Contract testing validates that API consumers and providers adhere to predefined agreements, preventing breaking changes in distributed systems.
In this blog, weβll cover:
- What contract testing is and why it’s essential
- Differences between contract testing, API stubbing, and mocking
- How to implement contract testing using Pact and OpenAPI Schema Validation
- Best practices for contract testing
- Useful GitHub repositories and AI-powered tools
π What is Contract Testing?
Contract testing ensures that the interaction between a service consumer (client) and provider (API server) adheres to a defined contract.
For example:
- A frontend app expects an API to return
{"id": 1, "name": "John Doe"}
when requesting/users/1
. - If the backend suddenly changes the response format, it might break the frontend.
- Contract testing prevents such breaking changes by ensuring both sides agree on the expected API structure.
π Contract Testing vs. API Stubbing vs. Mocking
Feature | Contract Testing | API Stubbing | Mocking (Unit Tests) |
---|---|---|---|
Purpose | Ensures API contract adherence | Simulates API behavior | Isolates function logic |
Scope | API-to-API communication | Integration tests | Unit tests |
Realism | Uses actual API contract | Uses stubbed API responses | Uses predefined return values |
Tools | Pact, OpenAPI Validators | responses, httpretty, WireMock | unittest.mock, pytest-mock |
Β
β Implementing Contract Testing in Pytest
1οΈβ£ Approach 1: Using OpenAPI Schema Validation
If your API follows OpenAPI (Swagger) specifications, you can validate responses against the OpenAPI contract.
πΉ How It Works
- The test calls
get_user(123)
, which fetches data from the API. validate_response(response)
checks if the response matches the OpenAPI schema.- If the API response does not match the schema, the test fails.
β Best For:
- Validating real API responses against OpenAPI contracts.
- Ensuring API changes do not break consumers.
# pip install pytest-openapi
# Consider an API with this OpenAPI Specification (openapi.json)
{
"openapi": "3.0.0",
"info": {
"title": "User API",
"version": "1.0.0"
},
"paths": {
"/users/{user_id}": {
"get": {
"summary": "Fetch user details",
"responses": {
"200": {
"description": "User found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
"email": {"type": "string"}
},
"required": ["id", "name", "email"]
}
}
}
}
}
}
}
}
}
# Now, letβs write a contract test using pytest-openapi.
import requests
import pytest
from pytest_openapi import validate_response
def get_user(user_id):
"""Function to fetch user data from API"""
return requests.get(f"https://api.example.com/users/{user_id}")
@pytest.mark.openapi("openapi.json")
def test_user_api_contract():
"""Contract test to validate API response against OpenAPI schema"""
response = get_user(123)
validate_response(response)
Β
π₯ 2οΈβ£ Approach 2: Consumer-Driven Contract Testing with Pact
Pact is a consumer-driven contract testing tool that ensures API consumers and providers stay in sync.
How Pact Works
- The consumer (client) defines expectations for the API.
- A Pact contract is generated based on these expectations.
- The provider (server) runs tests against the contract to ensure compliance.
πΉ How It Works
- The consumer generates a Pact file containing the expected API response.
- The provider runs tests against this contract to confirm that the API still behaves as expected.
β Best For:
- Microservices & distributed systems
- APIs used by multiple consumers
Step 1: Define the Consumer Contract (Stub API Response)
# Install Pact for Python
# pip install pact-python pytest
from pact import Consumer, Provider
import requests
pact = Consumer("UserConsumer").has_pact_with(Provider("UserProvider"))
def get_user_from_api(user_id):
return requests.get(f"http://localhost:5000/users/{user_id}")
def test_consumer_pact():
expected_response = {"id": 123, "name": "John Doe", "email": "john@example.com"}
with pact:
pact.given("User 123 exists").upon_receiving("A request for user 123").with_request(
method="GET", path="/users/123"
).will_respond_with(200, body=expected_response)
response = get_user_from_api(123)
assert response.json() == expected_response
Step 2: Verify the Contract on the Provider Side
# The API provider should run the following to verify the contract:
from pact.verify import ProviderVerifier
def test_provider_pact():
verifier = ProviderVerifier()
assert verifier.verify_pacts(pact_dir="./pacts", provider_base_url="http://localhost:5000")
π― Handling Edge Cases in Contract Testing
1οΈβ£ Testing for Missing Fields
If the API removes a required field, the contract test should fail.
@pytest.mark.openapi("openapi.json")
def test_missing_email():
response = get_user(123)
data = response.json()
assert "email" in data, "Email field is missing in API response"
2οΈβ£ Validating Response Formats
# Ensure that fields have the correct data types.
@pytest.mark.openapi("openapi.json")
def test_user_id_is_integer():
response = get_user(123)
assert isinstance(response.json()["id"], int)
3οΈβ£ Checking Backward Compatibility
# If an API provider introduces new optional fields, the contract should still pass.
@responses.activate
def test_new_optional_field():
responses.add(
responses.GET,
"https://api.example.com/users/123",
json={"id": 123, "name": "John Doe", "email": "john@example.com", "age": 30},
status=200
)
response = get_user(123)
assert "age" in response.json()
π Best Practices for Contract Testing
β
Keep contracts version-controlled β Store OpenAPI or Pact contracts in Git repositories.
β
Test API schema changes in CI/CD pipelines β Break builds if contracts fail.
β
Validate both request and response structures β Ensure that API clients send valid requests too.
β
Run contract tests before deploying API changes β Prevent breaking production consumers.
β
Use pytest-openapi
for schema validation and Pact for consumer-driven contracts.
π Further Reading & Resources
π Popular Resources
π― Underrated Resources
π GitHub Repositories
π€ AI Tools for Contract Testing
- Postman Contract Tests β Validate API contracts with test scripts
- Swagger Codegen AI β Automate API contract generation
π― Conclusion
Contract testing ensures APIs remain stable and compatible across updates. Using OpenAPI validation and Pact, developers can automate API schema verification and prevent breaking changes.
Would you like a deep dive into API versioning strategies next? π