Integration & Data

Consumer-Driven Contract Tests with Pact Python

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:

Python
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:

Python
# 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
The pact lifecycle across two repositories In the consumer repository, tests exercise the real client against the Pact mock server, which records each interaction into a pact file. The file is published. In the provider repository, the verifier fetches the pact, calls the state-setup endpoint for each interaction, replays the request against the running provider, and compares the response with the consumer's matchers, failing the provider's build on a mismatch. Written by the consumer, enforced on the provider consumer repository real client in a test Pact mock records pact JSON published provider repository set up state by name replay + compare real provider mismatch fails the provider's build
The dashed arrow is the whole point: the consumer's expectations travel to the repository where a breaking change would be made, and are enforced there.

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, EachLike and Term except 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.

Choosing a matcher from how the consumer uses a field Four rows. A field passed through or displayed uses Like. A collection uses EachLike with a minimum length. A formatted string the consumer parses uses Term with a regular expression. A value the consumer branches on uses a literal, and each literal is a promise the provider must keep exactly. Strictness should follow usage how the consumer uses it matcher provider may change displays or passes through Like the value iterates over a list EachLike length and values parses a formatted string Term value, not format branches on the value literal nothing
Reading a pact's literals is a quick audit of the consumer's real dependencies. Every one should correspond to an 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.

Python
# 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.

State vocabulary shared by many consumers Three consumer pacts each reference a small set of named states such as an open invoice exists and no invoice exists. The provider implements that vocabulary once, as a dictionary of setup functions. A state name not in the vocabulary fails verification with an explicit error rather than being ignored. A small vocabulary, many consumers checkout pact reporting pact admin pact provider STATES an open invoice exists a paid invoice exists no invoice exists unknown name fails loudly
When consumers use matchers, three or four general states cover nearly every interaction. A request for a highly specific state is a sign the consumer is asserting on data it should not care about.

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.

← Back to Contract Testing for HTTP APIs