Consumer-Driven Contract Testing
Your frontend team deploys a change that expects a new field in the user API response. The backend team deploys a refactor that renames that field. Both pass their unit tests. Both deploy successfully. Production breaks because nobody tested the interaction between them.
Integration tests catch this, but full integration test suites are slow, require all services running, and break when any service changes. Contract testing solves the specific problem of service-to-service compatibility — fast, isolated, and run in each service's own CI pipeline.
How consumer-driven contracts work
- Consumer defines expectations: "when I GET /users/123, I expect a 200 with
{ id, name, email }." - Consumer test runs against a mock provider that validates requests and returns canned responses per the contract.
- Contract file (JSON) is generated from the test interactions.
- Provider verification replays the contract against the real provider implementation.
- Both pass → safe to deploy independently.
Consumer test → Mock Provider → pact.json → Provider verification → ✅/❌
Consumer-side test with Pact
from pact import Consumer, Provider, Like, EachLike
import pytest
pact = Consumer("OrderService").has_pact_with(Provider("UserService"))
pact.start_service()
@pytest.fixture
def user_service():
(pact
.given("user 123 exists")
.upon_receiving("a request for user 123")
.with_request("GET", "/users/123")
.will_respond_with(200, body={
"id": Like("123"),
"name": Like("Jane Doe"),
"email": Like("[email protected]"),
}))
with pact:
yield pact
def test_get_user(user_service):
client = UserServiceClient(base_url=pact.uri)
user = client.get_user("123")
assert user.name == "Jane Doe"
assert "@" in user.email
Like("Jane Doe") means "a string matching this type," not "exactly this value." Contracts capture structure, not specific data.
Running this test generates order-service-user-service.json — the contract file.
Provider verification
The provider replays all interactions from the contract against its real API:
from pact import Verifier
def test_user_service_honors_contract():
verifier = Verifier(provider="UserService", provider_base_url="http://localhost:8080")
output, logs = verifier.verify_pacts(
"./pacts/order-service-user-service.json",
provider_states_setup_url="http://localhost:8080/_pact/provider_states",
)
assert output == (0, '')
Provider states set up test data:
@app.post("/_pact/provider_states")
def setup_provider_state(state: dict):
if state["state"] == "user 123 exists":
db.seed_user(id="123", name="Jane Doe", email="[email protected]")
return {"result": "success"}
Pact matchers for flexible contracts
Exact matching breaks on irrelevant changes. Matchers focus on structure:
from pact import Like, EachLike, Term, Format
body={
"id": Like("123"), # any string
"email": Term(r".+@.+\..+", "[email protected]"), # regex match
"created_at": Format().iso_8601_datetime, # format match
"tags": EachLike("premium"), # array of strings
"address": Like({ # nested object
"city": Like("Portland"),
"zip": Term(r"\d{5}", "97201"),
}),
}
Pact Broker for multi-team workflows
With multiple consumers, the broker tracks compatibility:
# CI pipeline: consumer publishes contract
- name: Publish pact
run: pact-broker publish ./pacts --consumer-app-version $GIT_SHA --broker-base-url $PACT_BROKER_URL
# CI pipeline: provider verifies against latest contracts
- name: Verify pact
run: |
pact-broker can-i-deploy --pacticipant UserService --version $GIT_SHA
pytest tests/test_provider_verification.py
can-i-deploy checks whether all consumer contracts are satisfied by the provider version about to deploy. Block deployment if verification fails.
Can-I-Deploy gate
The deployment safety check:
# Before deploying UserService v2.3.0
pact-broker can-i-deploy \
--pacticipant UserService \
--version 2.3.0 \
--to-environment production
# Checks:
# ✅ OrderService expects GET /users/{id} → 200 (verified)
# ✅ NotificationService expects GET /users/{id}/email → 200 (verified)
# ❌ AnalyticsService expects GET /users/{id}/preferences → 404 (NOT verified)
# Result: BLOCKED — AnalyticsService contract not satisfied
This prevents deploying a provider that breaks any consumer.
What contract testing does not cover
- Behavioral correctness — the provider returns the right shape but wrong data.
- Performance — contracts verify structure, not latency.
- New endpoints — contracts only cover interactions consumers actually use.
- Authentication flows — token exchange and OAuth require separate testing.
Contract testing complements — does not replace — unit tests, integration tests, and end-to-end tests.
Provider verification workflow
Consumer tests generate pacts; provider verification replays them:
# Provider side (pytest)
@pytest.fixture
def pact_verifier():
return Verifier(
provider="OrderService",
provider_base_url="http://localhost:8000",
)
def test_honors_consumer_contracts(pact_verifier):
pact_verifier.verify_with_broker(
broker_url=PACT_BROKER,
provider_version=GIT_SHA,
consumer_version_selectors=[{"mainBranch": True}],
)
Run provider verification in CI on every PR. Consumer tests run in consumer repos — the broker is the contract hub connecting both sides.
Breaking change workflow
Safe provider evolution:
- Add optional field → deploy provider → consumers unaffected
- Add required field → deploy provider first → update consumer pacts → deploy consumers
- Remove field → deprecate in provider, verify no consumer pact uses it → remove
Never remove a field while any consumer pact still expects it — can-i-deploy exists to enforce this ordering.
Pact vs OpenAPI contract testing
| Approach | Strength | Weakness |
|---|---|---|
| Pact | Tests actual consumer usage | Per-consumer maintenance |
| OpenAPI diff | Single spec source | Doesn't prove consumers use endpoints correctly |
| Schema registry (events) | Async message contracts | Different tooling |
Use Pact for synchronous HTTP between teams you don't control. Use shared OpenAPI when one team owns both sides.
Pair with CI/CD deployment strategies when can-i-deploy gates production releases.
Common production mistakes
Teams get contract testing pact wrong in predictable ways:
- Skipping failure-mode rehearsal — run a game day or fault injection exercise before peak traffic, not after the first outage.
- Missing correlation context — every error path should carry request, trace, or tenant identifiers so incidents are debuggable.
- Optimizing for demo, not steady state — load tests, cache warm-up, and cold-start paths matter more than local dev latency.
- Undocumented trade-offs — if you chose speed over strict correctness (or vice versa), write that down for the next engineer.
Production implementations of contract testing pact fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.
Resources
- Pact documentation
- Pact Python library
- Pact Broker documentation
- PactFlow (hosted Pact Broker)
- Martin Fowler: Consumer-Driven Contracts
Frequently asked questions
What is the difference between contract testing and integration testing?
Integration tests require both services running together — slow, flaky, and hard to maintain. Contract tests verify that a provider's API matches what consumers expect, using mocked interactions recorded by the consumer. Each service tests independently in CI, catching breaking changes before deployment.
Who writes the contract — the consumer or the provider?
The consumer writes the contract. The consumer defines what requests it sends and what responses it expects. The provider verifies it can satisfy those expectations. This inverts traditional API design where the provider defines the contract — consumer-driven means the provider cannot break consumers without explicit agreement.
Do I need a Pact Broker?
For one consumer and one provider, local contract files work. For teams with multiple consumers of the same provider, a Pact Broker (self-hosted or pactflow.io) stores contracts, tracks verification results, and provides a compatibility matrix showing which consumer-provider pairs are safe to deploy.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →