Integration tests prove that the consumer works against whatever the provider is running now. Consumer-driven contracts prove something more useful: that the provider still satisfies what its consumers depend on, checked in the provider's own pipeline, so a breaking change fails the build of the team that made it. The mechanism is a pact — a JSON file of request/response pairs written by the consumer's tests and replayed against the real provider.
Prerequisites
pact-python >= 2.2, which bundles the Pact mock server and verifier.- A consumer client that can be pointed at an arbitrary base URL, so the test can aim it at the Pact mock.
- The provider running in its own test pipeline, able to accept a state-setup call.
- The overview in contract testing for HTTP APIs.
Solution
The consumer side declares interactions and exercises its real client against the mock:
import atexit
import pytest
from pact import Consumer, Like, Provider, Term
from checkout.billing import BillingClient
pact = Consumer("checkout").has_pact_with(
Provider("billing"), host_name="localhost", port=1234, pact_dir="pacts"
)
pact.start_service()
atexit.register(pact.stop_service)
def test_reads_an_open_invoice():
expected = {
"id": Like("inv_123"), # any string
"total_minor": Like(1234), # any integer
"currency": Term(r"^[A-Z]{3}$", "GBP"), # matches the pattern
"status": "open", # literal: the consumer branches on it
}
(pact
.given("an open invoice exists")
.upon_receiving("a request for an invoice")
.with_request("GET", "/invoices/inv_123")
.will_respond_with(200, body=expected))
with pact: # verifies the request was made
invoice = BillingClient(base_url=pact.uri).fetch("inv_123")
assert invoice.is_open
The provider side replays every interaction against its real implementation:
# In the PROVIDER's repository
from pact import Verifier
def test_billing_satisfies_checkout(live_provider_url):
verifier = Verifier(provider="billing", provider_base_url=live_provider_url)
exit_code, _ = verifier.verify_pacts(
"pacts/checkout-billing.json",
provider_states_setup_url=f"{live_provider_url}/_pact/provider-states",
)
assert exit_code == 0
Why this works
During the consumer test, the Pact mock server answers requests according to the declared interactions and records exactly what the client sent. If the client sends something not declared, the test fails; if a declared interaction was never exercised, the with pact: block fails on exit. The resulting pact therefore describes real client behaviour, not an author's guess about it.
On the provider side, the verifier replays each recorded request against the running service after asking it to set up the named state, and compares the response to the consumer's matchers. Matchers are what keep this from becoming brittle: Like(1234) passes for any integer, so the provider's test data can differ from the consumer's example without failing verification.
Edge cases and failure modes
- Literal values everywhere. Every literal in a pact is a claim that the provider must return exactly that value. Use
Like,EachLikeandTermexcept where the consumer genuinely branches on the value. - Matching the whole response body. A pact mirroring every field the provider returns makes every additive change a failed verification. Declare only the fields the consumer reads.
- Over-specific provider states. "Invoice inv_9f3c exists with total 4211 in EUR" is a setup routine the provider must write for one consumer. Prefer a small vocabulary of general states.
- Unimplemented state names. A provider that silently ignores unknown states verifies interactions against the wrong data. Raise on an unknown state.
- Verifying draft pacts. A consumer's work-in-progress branch publishing pacts can fail the provider's main build. Verify only pacts tagged for deployed consumer versions.
Choosing matchers deliberately
Matchers are where most pacts go wrong, in both directions. Too strict and verification fails on harmless changes; too loose and the pact stops protecting anything. Four matchers cover nearly every case, and the choice between them follows from how the consumer uses each field.
Like(example) says "a value of this type". Use it for identifiers, amounts, names and timestamps the consumer passes through or displays without branching on them.
EachLike(example, minimum=1) says "a list whose items look like this". Use it for collections; the minimum expresses whether the consumer copes with an empty list, which is itself a contract worth stating.
Term(regex, example) says "a string matching this pattern". Use it for formatted values the consumer parses — currency codes, ISO dates, UUIDs — where the format matters but the value does not.
A literal says "exactly this value". Use it only where the consumer branches: an enum it switches on, a status that drives behaviour. Every literal is a promise the provider must keep, so each one should be a value the consumer genuinely cannot do without.
if or a match somewhere in the consumer's code.A practical review habit follows. For each literal in a new interaction, find the line of consumer code that branches on it. If there is none, the literal should be a matcher, and turning it into one removes a future false alarm from the provider's pipeline before it ever happens.
The inverse audit is just as useful on the provider side. When verification fails, the first question is whether the failing assertion is on a literal or a matcher. A failed matcher means the provider really did change a type or remove a field, which is a genuine breaking change to negotiate. A failed literal on a value the consumer does not actually branch on is a pact that was written too strictly, and the fix belongs in the consumer's repository rather than the provider's. Keeping that distinction clear stops contract testing from becoming a source of friction between teams, which is the usual reason it gets abandoned after an initial burst of enthusiasm.
Implementing provider states that stay fast
A pact with thirty interactions triggers thirty state setups during verification, and if each one truncates tables and reseeds, verification takes minutes. The provider-state endpoint should use the same transactional isolation as the rest of the provider's tests.
# Provider: a test-only endpoint the verifier calls before each interaction.
from fastapi import APIRouter, Depends
router = APIRouter()
STATES = {
"an open invoice exists": lambda db: db.add(Invoice(id="inv_123", status="open",
total_minor=1234, currency="GBP")),
"no invoice exists": lambda db: None,
}
@router.post("/_pact/provider-states")
def set_state(body: dict, db=Depends(get_test_session)):
state = body["state"]
if state not in STATES:
# Loud, not silent: an unknown state means a new consumer expectation.
raise ValueError(f"unknown provider state: {state!r}")
db.rollback() # discard the previous interaction's data
STATES[state](db)
db.flush()
return {"ok": True}
A small dictionary of named states, each a line or two, stays readable as consumers multiply — and a new state appearing in a consumer's pact fails verification with a clear message until someone deliberately adds it here.
Deciding whether a deploy is safe
The payoff of the full arrangement is a single question answered mechanically before every deploy: have all the consumers currently in production verified against the provider version about to ship? With a broker that records verifications, pact-broker can-i-deploy answers it directly and can gate the deploy step.
Without a broker the same question can still be answered, less elegantly, by verifying the pacts from each consumer's production tag rather than from their main branch. That distinction matters: a consumer's main branch may already depend on a provider change that has not shipped yet, and verifying against it would block the provider for a reason that is not a real incompatibility. Verifying against what consumers actually run is what makes the check trustworthy enough to gate on.
Either way, the result is a deploy that fails for a precise, attributable reason — "checkout 4.12 in production reads legacy_ref, which this build removes" — rather than an incident discovered from an error-rate graph. That is the whole argument for consumer-driven contracts, and it is only realised once verification is wired into the step that decides whether a deploy proceeds.
Frequently Asked Questions
Why use matchers instead of literal values in a pact? Because the consumer depends on the type and presence of a field, not on the particular value in one example. A literal total of 1234 fails verification the moment the provider's fixture uses 1250, which is a false alarm. A type matcher expresses exactly what the consumer needs.
Who writes the provider state setup? The provider team, in their own repository. The consumer names the state it needs — "an open invoice exists" — and the provider decides how to arrange it. Consumers describing how to create provider data would couple them to the provider's internals.
Do I need a Pact Broker? Not for one consumer and one provider; committing the pact file or publishing it as a build artefact works. A broker becomes worthwhile with several consumers, because it records which consumer versions have verified against which provider versions and answers whether a deploy is safe.
Related
- Contract Testing for HTTP APIs — how pacts relate to schema validation and end-to-end tests.
- Validating Responses Against an OpenAPI Schema — the cheaper structural check to run alongside.
- Database Fixtures & Transactional Tests — fast provider-state setup via rolled-back transactions.
- Modeling REST APIs as State Machines — exploring sequences of calls the pact does not enumerate.
← Back to Contract Testing for HTTP APIs