Quick Answer

A contract test checks that a provider's real response actually matches what a specific consumer expects, without either side needing the other running for the test to work. The consumer records its expectations as a contract; the provider replays that contract against its real response on every build. It catches breaking API changes — renamed fields, changed types, removed keys — at build time, before they reach a shared environment or production.

The gap between unit and end-to-end tests

Unit tests check one service in isolation, usually against a mock of whatever it depends on. They pass reliably — and they keep passing even after the real dependency changes shape, because the mock never noticed.

End-to-end tests check the whole system together, which does catch a mismatch, but only by running every service at once. They're slow, prone to flaking on infrastructure rather than logic, and by the time one fails in CI, the breaking change has usually already been merged and is sitting in a shared branch waiting to cause a bigger mess.

Contract testing sits between the two: it runs as fast as a unit test — no full environment needed — but it checks against a real, agreed-upon shape instead of a hand-written mock that can quietly drift out of date. That's the specific gap it closes.

Consumer-driven contracts

The common approach (popularized by the Pact framework) is consumer-driven: the team that consumes an API defines the contract, because they're the ones who know exactly which fields and shapes they actually depend on.

The flow: the consumer's test suite generates a contract file describing the requests it makes and the response shape it expects back. That contract is shared with the provider team — committed to a shared repo, or published to a broker service built for this.

The provider's own CI pipeline then replays the contract's requests against its real, running code, and checks the real response against what the contract expects. If a field got renamed or a type changed, the provider's build fails — not the consumer's production deployment, weeks or months later.

This is what "consumer-driven" means in practice: the provider doesn't get to unilaterally decide its API surface is fine just because its own tests pass. It has to keep satisfying every contract that real consumers have actually published against it, or the build tells it exactly which one broke and why.

Neither side needs the other actually running to test against. The contract is the stand-in for "here's what the other side needs," verified independently by each.

A contract check, passing and failing — real run

A simplified version: a contract describing the required shape of an order response, checked against two different provider responses.

const orderContract = {
  type: 'object',
  required: ['id', 'status', 'total', 'items'],
  properties: {
    id: { type: 'string' }, status: { type: 'string' },
    total: { type: 'number' },
    items: { type: 'array', items: { type: 'object',
      required: ['sku', 'qty'],
      properties: { sku: { type: 'string' }, qty: { type: 'number' } } } }
  }
};

Checking a valid response against it, and then a response where the provider renamed total to totalAmount and switched status to a numeric code:

[PASS] GET /orders/ord_99 (v1 provider): response satisfies the contract

[FAIL] GET /orders/ord_100 (v2 provider, breaking change): response violates the contract
   - status: expected string, got number (2)
   - total: required field missing

That's a real, caught mismatch — exactly the kind of change that would otherwise reach a mobile app or frontend expecting the old shape and fail there instead.

What contract testing won't catch

A contract only protects the fields it actually lists. If a consumer never declared that it reads a particular field, the provider can rename or remove that field freely and the contract test stays green — even if some other, unlisted consumer breaks in production. Contract testing is only as strong as consumers being thorough about declaring what they use.

It also checks shape, not full business meaning. A field that's still present and still the correct type, but now means something subtly different, won't be caught unless the contract's example values are specific enough to expose it.

And contracts need to actually reach the provider team, versioned, whenever they change — a broker service (Pact Broker is the common one) or a shared repository both teams pull from. Without that, a consumer can update its expectations and the provider never finds out until its own contract test run picks up the new version, if it's set up to check at all.

Where this earns its keep

Contract testing pays off between independently deployed services owned by different teams — exactly the situation where an end-to-end environment covering both is expensive to maintain, and where you'd rather the breaking change fail in the provider's own CI than get discovered after both sides are already deployed. It's especially valuable for public or partner-facing APIs, where you may not even know every consumer well enough to test against them directly, but a published contract still catches the changes that matter most.

It's less necessary inside a single codebase or monorepo where a shared type system and the compiler already catch most of these breaks for free at build time — that's effectively a contract check the language is doing for you automatically. Reach for explicit contract testing once services genuinely can't share types directly across a network boundary, which in practice means the moment two teams start deploying independently on their own schedules.

Frequently Asked Questions

What is contract testing? A way of testing that a service's API response matches what a specific consumer of it actually expects, checked independently on each side without both services needing to run together.
How is contract testing different from integration testing? Integration testing runs real dependent services together and is slow and environment-heavy. Contract testing checks each side against a shared, recorded expectation, and runs as fast as a unit test.
What is Pact? The most widely used consumer-driven contract testing framework. Consumers generate a contract from their tests, and providers verify their real responses against it in their own CI pipeline.
Do both the consumer and provider need to run the tests? Yes, but not against each other. The consumer generates the contract from its own tests; the provider separately verifies its real response against that same contract in its own pipeline.
Is contract testing a replacement for end-to-end tests? No. It catches shape and schema breaks earlier and faster, but it doesn't replace end-to-end coverage for testing real cross-service business flows and timing.