A comprehensive example demonstrating Consumer-Driven Contract Testing using Pact in Python with FastAPI.
Contract testing is a technique for testing the integration points between services (consumers and providers) by verifying that both sides of the contract are compatible. Unlike integration tests that test the entire system, contract tests focus on the interface between services.
The Consumer is the service that uses (consumes) an API. It's typically the client application that makes HTTP requests to another service. In contract testing, the consumer:
- Defines what it expects from the provider
- Creates contracts based on its expectations
- Tests against a mock provider to ensure its expectations are met
- Publishes contracts to the broker for provider verification
The Provider is the service that exposes an API for others to consume. It's the server that responds to HTTP requests. In contract testing, the provider:
- Implements the actual API endpoints
- Verifies that it can fulfill the contracts published by consumers
- Tests its real implementation against consumer expectations
- Publishes verification results back to the broker
A Contract (or Pact) is a formal agreement between a consumer and provider that specifies:
- The expected request format (HTTP method, path, headers, body)
- The expected response format (status code, headers, body structure)
- Any provider states or scenarios that must be satisfied
- Version information for tracking contract evolution
Contracts serve as living documentation and are used to detect breaking changes before they reach production.
The Broker is a centralized repository that stores and manages contracts between consumers and providers. It:
- Stores contracts published by consumers
- Provides contracts to providers for verification
- Tracks verification results and contract versions
- Acts as a single source of truth for all contract information
- Early Detection: Catch breaking changes before they reach production
- Confidence: Refactor with confidence knowing contracts will catch issues
- Documentation: Contracts serve as living documentation of API expectations
- Consumer-Driven: The consumer drives what the API should look like
Consumer comes first
┌─────────────────┐ Defines Expectations ┌─────────────────┐
│ Consumer │ ─────────────────────────► │ Pact Broker │
│ (API Client) │ │ (Contract │
│ │ │ Repository) │
└─────────────────┘ └─────────────────┘
│ │
│ Creates Pact │ Stores Contract
│ (Contract) │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Mock Server │ │ Provider │
│ (Simulates │ │ (API Server) │
│ Provider) │ │ │
└─────────────────┘ └─────────────────┘
- Consumer defines it's expectation and mock the provided response using Mock Server provided by Pact, which generates the contract file.
- Consumer publishes the generated contracts to the Pact Broker which acts like a storage for keeping and sharing different contract versions between the consumer and provider.
- Provider talks to the broker and verifies the contract states or scenarios with it's own mocked version of actual endpoint, defined in
/_pact/provider_states
. - Finally, Provider publishes the verification result to the broker.
sequenceDiagram
participant C as Consumer
participant M as Mock Server
participant B as Pact Broker
participant P as Provider
Note over C,P: Phase 1: Consumer Creates Contract
C->>M: Define expected interactions
M->>C: Simulate provider responses
C->>B: Publish pact (contract)
Note over C,P: Phase 2: Provider Verifies Contract
P->>B: Fetch pacts for verification
B->>P: Return consumer expectations
P->>P: Test against real implementation
P->>B: Publish verification results
Note over C,P: Phase 3: Breaking Change Detection
alt Breaking Change
P->>B: Verification fails
B->>P: Contract violation detected
else No Breaking Change
P->>B: Verification passes
B->>P: Contract satisfied
end
┌──────────────────────────────────────────────────────────┐
│ Consumer Test │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ API Client │───►│ Mock Server │ │
│ │ │ │ │ │
│ │ Expects: │ │ Simulates: │ │
│ │ - service │ │ - service │ │
│ │ - version │<---│ - version │ │
│ │ - build │ │ - build │ │
│ └─────────────────┘ └─────────────────┘ │
└──────────────────────────────────────────────────────────┘
│ ✅ PASSED + PUBLISHED TO BROKER
▼
┌──────────────────────────────────────────────────────────-┐
│ Provider Verification │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Pact Broker │───►│ Real Provider │ │
│ │ │ │ │ │
│ │ Expects: │ │ Returns: │ │
│ │ - service │ │ - service │ │
│ │ - version │<---│ - version │ │
│ │ - build │ │ - no build │ │
│ └─────────────────┘ └─────────────────┘ │
└───────────────────────────────────────────────────────────┘
│ ❌ FAILS (build is required)
▼
Breaking Change Detected!
contract-testing-poc/
├── src/
│ ├── consumer/ # API Client (Consumer)
│ │ ├── sync_service_client.py # HTTP client for provider
│ │ ├── test_sync_service_consumer.py # Consumer tests
│ │ ├── pacts/ # Generated pact files
│ │ └── pact-logs/ # Pact test logs
│ └── provider/ # API Server (Provider)
│ ├── main.py # FastAPI application
│ ├── sync_controller.py # API endpoints
│ ├── test_sync_provider.py # Provider verification tests
│ └── pact-logs/ # Verification logs
├── scripts/ # Build and test scripts
├── docker-compose.yml # Pact Broker setup
├── Makefile # Project commands
└── requirements.txt # Python dependencies
- Python 3.11+
- Docker and Docker Compose
- Make (optional, but recommended)
# Start the Pact Broker (contract repository)
make start-broker
This starts a Pact Broker at http://localhost:9292
where contracts are stored and shared.
# Install Python dependencies
make install
This command:
- Creates new virtual environment
.venv
- Install necessary dependencies from
requirements.txt
file
Recommended: make init
You can also run
Step 1
andStep 2
together withmake init
It also cleans up existing pact files
# Run the consumer tests to generate the contracts and publish to the broker
make test
This command:
- Cleans previous pact files
- Runs consumer tests (creates contracts)
- Publish the contracts to the Broker
# Run provider tests to verify published contracts as well as push the verification result to the broker
make verify
This command:
- Runs provider verification (test contracts against real API)
- Publish verification result to the broker
# Recommended for initial setup
make init
# Start Pact Broker
make start-broker
# Stop Pact Broker
make stop-broker
# Install dependencies
make install
# Clean pact files
make clean-pacts
# Run consumer tests only and publish contracts to the broker
make test
# Run provider verification only
make verify
# Check if consumer can be deployed
make can-i-deploy-consumer
# Check if provider can be deployed
make can-i-deploy-provider
# Deploy consumer
make deploy-consumer
# Deploy provider
make deploy-provider
This includes
- Creating Mock Server
- Configuring Broker with
publish_to_broker=True
- Feed the mock server with response that the consumer expects from the provider
- Assert the response from the mock server
# import all necessary pact components
from pact import Consumer, Provider, Term, Format, Like, Pact
# Create Pact Mock Server
@pytest.fixture(scope="session")
def mock_server(
# Fixtures
pact_dir,
pact_log_dir,
broker_opts,
contract_version,
contract_branch
):
pact: Pact = Consumer(
CONSUMER_NAME,
version=contract_version,
branch=contract_branch
).has_pact_with(
provider=Provider(PROVIDER_NAME),
host_name=MOCK_SERVER_HOST,
port=MOCK_SERVER_PORT,
broker_base_url=PACT_BROKER_URL,
broker_username=PACT_BROKER_USERNAME,
broker_password=PACT_BROKER_PASSWORD,
publish_to_broker=True, # This is important to publish contracts right away
)
try:
pact.start_service()
yield pact
finally:
pact.stop_service()
@pytest.mark.contract
@pytest.mark.asyncio
async def test_get_version(mock_server):
# Consumer defines what it expects from the provider
expected_version = {
'service': Term(
matcher=r'^sync-service$',
generate='sync-service'
),
'version': Term(
matcher=r'^\d+(.\d+){2,3}$',
generate='1.0.0'
),
'build': Term(
matcher=r'^\d{8}-[a-f0-9]+$',
generate='20240101-abc123'
)
}
# Feed the mock server with expected response from /version endpoint.
# This is crucial because it generates the contract and acts as a mock provider
(
mock_server
.given('sync-service is running')
.upon_receiving('a request for version information')
.with_request(
method='GET',
path='/version'
)
.will_respond_with(
status=200,
headers={'Content-Type': 'application/json'},
body=Like(expected_version)
)
)
# Test against mock server
with mock_server:
resp = await client.get_version()
# assert response
assert resp.service is not None and isinstance(resp.service, str)
assert resp.version is not None and isinstance(resp.version, str)
assert resp.build is not None and isinstance(resp.build, str)
assert resp.service == 'sync-service'
Checkout the full code example here: https://github.com/dipanjal/contract-testing-poc/blob/main/src/consumer/test_sync_service_consumer.py
This includes:
- Fetch contracts from the Broker
- Cross-match contract states / scenarios with provider states from endpoint
/_pact/provider_states
- Assert verification result
SUCCESS=0
andFAIL=1
- Publish the verification result to the Broker
# Provider verifies against real implementation
verifier = Verifier(
provider="sync-service",
provider_base_url="http://localhost:5000"
)
result = verifier.verify_with_broker(
broker_url="http://localhost:9292",
# ... other options
)
assert result == 0 # Passes if no breaking changes
Checkout the full code example here: https://github.com/dipanjal/contract-testing-poc/blob/main/src/provider/test_sync_provider.py
If any response schema change in the Provider
doesn't match with the contract published by the Consumer
,
it will be considered as a BREAKING CHANGE!
This check can be done by verifying teh Provider after schema changes.
# verify the provider after response schema changed
make verify
Before:
{
"service": "sync-service",
"version": "1.0.0",
"build": "20240101-abc123",
"timestamp": "2025-07-24T15:43:24.204757Z"
}
After (Breaking Change):
{
"service": "sync-service",
"version": "1.0.0",
"timestamp": "2025-07-24T15:43:24.204757Z"
// build removed - BREAKING CHANGE!
}
Result: ❌ Provider verification fails
Before:
{
"build": "20240101-abc123" # String
}
After (Breaking Change):
{
"build": 20240101 # Number - BREAKING CHANGE!
}
Result: ❌ Provider verification fails
Before:
{
"service": "sync-service",
"version": "1.0.0",
"build": "20240101-abc123",
"timestamp": "2025-07-24T15:43:24.204757Z"
}
After:
{
"service": "sync-service",
"version": "1.0.0",
"build": "20240101-abc123"
// timestamp removed - NON BREAKING CHANGE
}
Result: ✅Provider verification succeeded
If you take a look at the contract published by the consumer http://localhost:9292/pacts/provider/sync-service/consumer/transaction-service/latest
You will see, consumer don't expect to have timestamp
in the response.
Therefore, any removal or changes in the timestamp
field wouldn't break our consumer code.
- Let consumers define what they need
- Providers should adapt to consumer requirements
- Use contracts as living documentation
- Always make changes backward compatible
- Use optional fields for new features
- Version APIs when breaking changes are necessary
- Add new fields as optional
- Deprecate old fields gradually
- Communicate changes to all consumers
- Run consumer tests in CI/CD
- Run provider verification before deployment
- Use "can-i-deploy" checks for safety
-
Pact Broker Not Running
make start-broker
-
Provider Service Not Running
# Check if service is running on port 5000 curl http://localhost:5000/health
-
Authentication Issues
- Default credentials:
pactbroker
/pactbroker
- Check
envs/local.env
for demo configuration
- Default credentials:
-
Test Failures
- Check pact logs in
src/*/pact-logs/
- Verify provider is returning expected schema
- Ensure all required fields are present
- Check pact logs in
# Run with verbose logging
PACT_LOG_LEVEL=DEBUG make contract-test
# Pact Broker Configuration
PACT_BROKER_URL=http://localhost:9292
PACT_BROKER_USERNAME=pactbroker
PACT_BROKER_PASSWORD=pactbroker
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all contract tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.