Contract Testing with Pact
A comprehensive example demonstrating Consumer-Driven Contract Testing using Pact in Python with FastAPI.
Problems with End to End Testing
While E2E testing has its place, it comes with significant challenges when testing API integrations between microservices.
- Slow Execution
Full E2E test suites can take 30+ minutes to complete, slowing down development feedback loops. - Flaky Tests
Tests fail randomly due to network issues, timeouts, or service dependencies that are outside the control of the test. - Complex Setup
Requires starting entire infrastructure including databases, message queues, and external services. - Hard to Debug
Difficult to isolate which specific service or component caused the test failure. - Expensive Resources
Needs full staging environment with all dependencies running simultaneously. - Slow Feedback Loop
Developers wait hours to know if their changes broke anything in the integration. - Tight Coupling
Changes in one service can break tests in completely unrelated services. - Limited Scope
Only tests the happy path and a few error scenarios, missing edge cases. - Maintenance Overhead
High cost to maintain and update tests when APIs change.
What is Contract Testing?
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.
Key Players
Consumer
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
Provider
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
Contract
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.
Broker
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
Why Contract Testing?
- 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
How Pact Testing Works
1. Consumer-Driven Approach
Consumer Comes First
2. Contract Testing Flow
- 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
3. Breaking Change Detection
Quick Start
Github Repository: https://github.com/dipanjal/contract-testing-poc
Project Structure
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
Prerequisites
- Python 3.11+
- Docker and Docker Compose
- Make (optional, but recommended)
1. Start the Pact Broker
# Start the Pact Broker (contract repository)
make start-broker
This starts a Pact Broker at http://localhost:9292
where contracts are stored and shared.
2. Install Dependencies
# 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
3. Run Consumer Tests
# 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
4. Run Provider Tests
# 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
Available Commands
Basic Commands
# 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
Testing Commands
# Run consumer tests only and publish contracts to the broker
make test
# Run provider verification only
make verify
Deployment Commands
# 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
Understanding the Tests
Consumer Test Example
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()
Now We will be using the mock server to generate our contract and simulate the provider response
@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 SimpleSyncServiceClient(MOCK_SERVER_URL).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
Provider Verification Example
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
Breaking Change Scenarios
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 the Provider after schema changes.
# verify the provider after response schema changed
make verify
1. Removing a Required Field
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
2. Changing Field Type
Before:
{
"build": "20240101-abc123" # String
}
After (Breaking Change):
{
"build": 20240101 # Number - BREAKING CHANGE!
}
Result: ❌ Provider verification fails
3. Removing Unnecessary Field
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.
Best Practices
1. Consumer-Driven Design
- Let consumers define what they need
- Providers should adapt to consumer requirements
- Use contracts as living documentation
2. Backward Compatibility
- Always make changes backward compatible
- Use optional fields for new features
- Version APIs when breaking changes are necessary
3. Contract Evolution
- Add new fields as optional
- Deprecate old fields gradually
- Communicate changes to all consumers
4. Testing Strategy
- Run consumer tests in CI/CD
- Run provider verification before deployment
- Use "can-i-deploy" checks for safety
Troubleshooting
- 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
(you can change it anytime you want) - 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
Debug Mode
# Run with verbose logging
PACT_LOG_LEVEL=DEBUG make contract-test
Environment Variables
A sample local.env
is provided, you can follow them to customize on your own
# Pact Broker Configuration
PACT_BROKER_URL=http://localhost:9292
PACT_BROKER_USERNAME=pactbroker
PACT_BROKER_PASSWORD=pactbroker