Skip to content

Contract Testing: Consumer-Driven Confidence

The previous page, Integration Strategies, assumed you could wire the pieces together in one test — big-bang or incremental, but all in one room. Modern systems break that assumption. Your checkout service and your payment service live in different repos, ship on different days, and are owned by different teams. You often cannot stand both of them up in a single test run at all.

So a nagging question appears: if I deploy my service today and you deploy yours next week, how do I know we still agree on the messages we exchange — before a customer finds out for us? Contract testing is the answer, and it earns its confidence in a surprising way: by never running both services at the same time.

The problem: green tests on both sides, a broken system in the middle

Section titled “The problem: green tests on both sides, a broken system in the middle”

Picture two services that talk over HTTP. The consumer (an order service) calls the provider (a user service) to fetch a customer’s shipping address.

Order service User service
(consumer) (provider)
│ GET /users/42 │
│ ──────────────────────────────► │
│ { "id": 42, │
│ "address": {...} } │
│ ◄────────────────────────────── │

Each team writes its own tests. The provider team has unit tests proving GET /users/42 returns the right JSON. The consumer team has unit tests proving that given that JSON, an order gets a correct shipping label. Both suites are green. Both deploys go out. And the system is broken.

How? The provider team renamed address to shippingAddress in a refactor. Their tests still pass — they updated the tests to match the new field. The consumer team’s tests also still pass — they mock the provider’s response, and their mock still says address. Nobody ran a test that saw both the real new response and the real consumer parsing it. The interface drifted, and every test on Earth stayed green.

This is the gap contract testing fills. Unit tests on either side verify a service against its own idea of the interface. Nothing verifies that the two ideas still match.

Consumer-driven contracts: the consumer writes down what it needs

Section titled “Consumer-driven contracts: the consumer writes down what it needs”

The key move is to make the interface an explicit, shared artifact instead of two private assumptions. And the surprising part — the “consumer-driven” part — is who writes it: the consumer does.

The logic is: a provider can return a hundred fields, but a given consumer only uses three of them. What actually needs to stay stable is not the provider’s entire response — it is the subset of it that some real consumer depends on. So we let the consumer state its expectations, and those expectations become the contract.

1. Consumer test runs against a MOCK provider.
While running, it RECORDS every request it makes
and every response it relies on.
2. That recording is the CONTRACT
("when I GET /users/42, I need a body
with id:int and address.street:string").
3. Provider replays the contract against the
REAL provider and checks it can satisfy it.

Notice what the contract captures: only the fields the consumer actually reads. If the provider adds new fields, the contract is unbothered. If the provider removes or renames a field the consumer reads — the drift from our story — the provider-side replay fails immediately.

A contract is just data — typically JSON. Conceptually it is a list of interactions, each pairing a request the consumer makes with the response shape it depends on:

{
"consumer": "order-service",
"provider": "user-service",
"interactions": [
{
"description": "a request for user 42",
"request": { "method": "GET", "path": "/users/42" },
"response": {
"status": 200,
"body": {
"id": 42,
"address": { "street": "12 Baker St" }
},
"matchingRules": {
"body.id": "type:integer",
"body.address.street": "type:string"
}
}
}
]
}

The matchingRules matter: the contract usually asserts types and structure, not exact values. It says “there must be an integer id and a string address.street,” not “the id must literally be 42.” That keeps the contract about the shape of the agreement rather than about any one test fixture.

The workflow: consumer generates, broker shares, provider verifies

Section titled “The workflow: consumer generates, broker shares, provider verifies”

Tools in the Pact family (Pact being the most common consumer-driven contract framework as of early 2026) turn the idea above into a CI pipeline with three actors.

┌──────────────┐ publishes ┌─────────┐ pulls ┌──────────────┐
│ CONSUMER CI │ ────────────► │ BROKER │ ─────────► │ PROVIDER CI │
│ │ contract │ (shared │ contract │ │
│ runs tests │ │ store) │ │ replays it │
│ vs mock, │ ◄──────────── │ │ ◄───────── │ vs real │
│ emits pact │ "can I │ verifi- │ publishes │ service, │
│ │ deploy?" │ cation │ result │ reports pass │
└──────────────┘ result └─────────┘ └──────────────┘
  1. Consumer CI runs the consumer’s tests against a local mock. A passing run emits the contract file and publishes it to the broker, tagged with the consumer’s version.
  2. The broker is a shared store of contracts and verification results. It is the single place both teams look to ask “do the current versions agree?”
  3. Provider CI pulls every contract naming it as a provider and replays each interaction against the real, running provider. It reports pass/fail back to the broker.

The payoff is fail fast on a breaking change. When the provider team renames address to shippingAddress, provider CI replays the order-service contract, sends the recorded GET /users/42, and finds no address.street in the response. Provider CI goes red — in the provider’s own pipeline, before merge. The break is caught by the side that caused it, at the moment they caused it.

The broker also answers the deployment question directly. Before a deploy, a can-i-deploy check asks the broker: “for the exact versions about to go to production, has every contract between them been verified?” If not, the deploy is blocked. That is how two independently-released services stay honest without ever meeting in a test.

It is tempting to see contract testing as a weaker integration test. It is better to see it as a different tool that trades a little realism for a lot of independence.

Full integration testContract test
Services running at onceBoth (plus their databases)One
What it provesThe real end-to-end path worksThe two sides agree on the interface
Runs when the other team is deployingNo — needs their live serviceYes — replays a recorded contract
Catches business-logic bugs across servicesYesNo — only interface mismatches
Speed / flakinessSlower, more moving partsFaster, fewer moving parts

A full integration test (next page: Real vs Fake Dependencies) can catch things a contract test never will — a bug in how the provider computes the address, a database constraint, a timeout. A contract test cannot; it only proves the messages line up. But it proves that cheaply, in isolation, and continuously — which is exactly the property you need when you cannot boot both services together.

They are complements, not rivals. Contracts guard the interface on every commit; a small number of end-to-end tests (see End-to-End Tests and Their Cost) guard a few critical journeys less often.

Under the hood — why the mock and the replay must share the contract

Section titled “Under the hood — why the mock and the replay must share the contract”

The reason consumer-driven contracts actually catch drift is that the same artifact drives both sides. The consumer’s mock is not hand-written to look like the provider — it is generated from the contract. The provider’s verification is not hand-written to look like the consumer — it is generated from the same contract.

That shared origin is what closes the gap from our opening story. In that story, the consumer’s mock and the provider’s reality drifted independently because nothing forced them to agree. Here, if the consumer’s mock said address, the contract says address, and the provider must satisfy address or fail. There is exactly one description of the interface, and both sides are tested against it. Remove the shared artifact and you are back to two private assumptions — back to green tests and a broken system.

Contract testing has real setup cost: a broker to run, a testing framework on both sides, and pipeline wiring. It earns that cost under specific conditions.

It pays off when:

  • You have many services talking over HTTP or messaging, so full end-to-end tests are slow and combinatorially painful.
  • The services are owned by separate teams, so no one person can see both sides of an interface change.
  • The services deploy independently, so “we’ll just test them together before release” is not available — there is no shared release.

It is overkill when:

  • You have a single monolith. If both sides of a call are in the same codebase and deploy together, an ordinary in-process integration test already sees the real caller and the real callee. There is no independent drift to guard against, so a contract adds ceremony with no new safety.
  • You have one team and one deploy for a couple of tightly-coupled services — a shared integration test is often simpler than standing up a broker.

The rule of thumb: contract testing buys you safe independent deployment. If your services do not deploy independently, you are paying for a guarantee you are not using.

  • Why does it exist? Because independently-deployed services can drift apart on their shared interface even when every test on both sides is green, and nobody wants to boot the whole system on every commit to notice.
  • What problem does it solve? It verifies that a consumer and a provider still agree on the messages they exchange, using a shared, explicit contract — while only ever running one side at a time.
  • What are the trade-offs? You gain fast, isolated, deploy-safe interface checks; you give up realism (no cross-service business logic is exercised) and take on the cost of a broker and framework on both sides.
  • When should I avoid it? In a single monolith, or when tightly-coupled services share one deploy — an ordinary integration test already sees both real sides, so a contract is pure overhead.
  • What breaks if I remove it? You lose the guarantee that independent deploys stay compatible; interface drift returns to being invisible until an end-to-end test, or a customer, hits it in production.
  1. In the opening story, both the consumer’s and the provider’s own test suites were green, yet the system was broken. What did neither suite test?
  2. Why is the contract consumer-driven — what does letting the consumer record its expectations buy you that a provider-written spec would not?
  3. Walk the three-actor workflow: what does the consumer publish, where does it go, and what does the provider do with it?
  4. Give one kind of bug a full integration test catches that a contract test never will, and one advantage the contract test has in return.
  5. A team has a single monolith with an internal module boundary. Should they add consumer-driven contract testing across that boundary? Why or why not?
Show answers
  1. Neither suite tested the real provider response against the real consumer parsing it. Each side tested itself against its own idea of the interface — the provider updated its own tests to the new field name, the consumer’s mock still used the old one — so the drift between the two ideas was never exercised.
  2. Because a consumer only depends on the subset of the response it actually reads. Letting the consumer record exactly those expectations means the contract protects what is truly relied upon: the provider is free to add fields, but removing or renaming a field a real consumer uses fails immediately. A provider-written spec would guard fields no one uses and might miss the ones that matter.
  3. The consumer runs its tests against a mock and publishes the resulting contract to the broker, tagged with its version. The broker stores contracts and verification results. Provider CI pulls every contract naming it, replays each recorded request against the real provider, checks the response satisfies the contract, and reports pass/fail back to the broker.
  4. A full integration test catches cross-service business-logic or infrastructure bugs — e.g. the provider computing the wrong address, a DB constraint, or a timeout — which a contract test cannot see. In return, the contract test needs only one service running, so it is faster, less flaky, and can run even while the other team’s service is mid-deploy.
  5. Probably not. If both sides of the boundary live in one codebase and deploy together, an ordinary in-process integration test already exercises the real caller and the real callee, so there is no independent drift to guard against. Contract testing buys safe independent deployment; a monolith does not deploy the two sides independently, so the broker and framework are cost without benefit.