Testing in Practice · Testing APIs, Contracts and User Journeys
Consumer-Driven Contract Tests
Catch breaking API changes before deployment by checking the provider against the fields and types its consumers actually use.
What you'll build
A contract between a mobile app (consumer) and an orders API (provider), written as a JSON Schema of what the app relies on. The provider's tests verify its real responses against the contract, so a "harmless" rename of a field fails CI instead of crashing the app.
The idea
- Integration tests across teams are slow and brittle; contract tests check each side independently against an agreed contract.
- Consumer-driven: the consumer states what it needs (fields, types, required-ness); the provider must keep satisfying it. Extra fields are fine.
- Tools like Pact record contracts from consumer tests and verify them in provider CI; this example shows the principle with JSON Schema.
The code
# file: contract.py
# What the mobile app reads from GET /orders/{id}: owned by the consumer team
ORDER_CONTRACT = {
"type": "object",
"required": ["id", "status", "total", "items"],
"properties": {
"id": {"type": "string"},
"status": {"enum": ["pending", "paid", "shipped", "cancelled"]},
"total": {"type": "object", "required": ["amount", "currency"],
"properties": {"amount": {"type": "integer"}, "currency": {"type": "string"}}},
"items": {"type": "array", "items": {"type": "object", "required": ["sku", "quantity"]}},
},
}
# file: provider.py
def get_order_v1(order_id):
return {"id": order_id, "status": "paid", "total": {"amount": 2400, "currency": "EUR"},
"items": [{"sku": "teapot", "quantity": 1}], "internal_ref": "x9"}
def get_order_v2(order_id): # a refactor: "total" became "price", status gained a new value
return {"id": order_id, "status": "refunded", "price": {"amount": 2400, "currency": "EUR"},
"items": [{"sku": "teapot", "quantity": 1}]}
# file: test_contract.py
import pytest
from jsonschema import Draft202012Validator
from contract import ORDER_CONTRACT
from provider import get_order_v1, get_order_v2
@pytest.mark.parametrize("handler", [get_order_v1, get_order_v2], ids=["v1", "v2"])
def test_provider_honours_the_mobile_contract(handler):
errors = sorted(e.message for e in Draft202012Validator(ORDER_CONTRACT).iter_errors(handler("o1")))
assert not errors, "\n".join(errors)
# file: main.py
import pytest
pytest.main(["-q", "--tb=short", "-p", "no:cacheprovider"])
Output
.F [100%]
=================================== FAILURES ===================================
________________ test_provider_honours_the_mobile_contract[v2] _________________
test_contract.py:9: in test_provider_honours_the_mobile_contract
assert not errors, "\n".join(errors)
E AssertionError: 'refunded' is not one of ['pending', 'paid', 'shipped', 'cancelled']
E 'total' is a required property
E assert not ["'refunded' is not one of ['pending', 'paid', 'shipped', 'cancelled']", "'total' is a required property"]
=========================== short test summary info ============================
FAILED test_contract.py::test_provider_honours_the_mobile_contract[v2] - Asse...
1 failed, 1 passed in <time>Run this on your computer — see “Get Started” for setup.
Version 1 passes (the extra internal_ref field doesn't matter). Version 2 fails with both breaking changes listed, before any app user sees them.
Common mistakes
- Provider-written contracts that describe everything the API returns, so any change "breaks" them.
- Contracts that live only in a wiki; they must run in CI on both sides.
- Treating contract tests as functional tests; they check shape and semantics consumers rely on, not business logic.
Key points
- Consumers define what they rely on; providers verify it in CI.
- Extra fields are allowed; removing, renaming or retyping used fields breaks the contract.
- Tools like Pact automate publishing and verifying contracts.
Further reading: Pact documentation · Martin Fowler: consumer-driven contracts · JSON Schema
Practice questions
What is a consumer-driven contract?
Answer
A description, owned by the consumer, of the parts of a provider's API it relies on; the provider verifies it in its own CI.
Why do contract tests allow extra fields?
Answer
Consumers ignore fields they don't use, so adding fields is compatible; only removing or changing used fields breaks them.
What problem do contract tests solve compared with end-to-end tests across services?
Answer
They catch interface breaks quickly and independently in each team's CI, without deploying and wiring all services together.
Open in the app to mark lessons complete, take notes and track your progress.