Back to Blog
On this page5 sections

// deep dive

Contract testing, explained without the Pact marketing

qa.codesqa.codes · 11 November 2025 · 10 min read
Advanced
contract-testingpactapi-testingmicroservices

Contract testing is two things wearing one name: a model and a tool. The model is genuinely useful; the marketing for the tool oversells where it fits. Here's the model, separated from any vendor's pitch.

part ofAPI bugs QA should catch

The model in plain language

A contract, in the testing sense, is a shared specification of what data passes between two services. The consumer — the service that calls the API — says "I need the response to look like this." The producer — the service that serves the API — says "I produce responses that look like this." Contract testing is the practice of asserting that both sides agree on that spec.

The key insight: if you want to know whether service B's API is compatible with service A's expectations, you don't need to run both services together. You can test them separately, as long as both sides are tested against the same contract document.

Consumer A  ──tests against──>  [Mock shaped by contract]
Producer B  ──tests against──>  [Contract document]
                                [The shared spec: request + response shape]

When both sides pass their respective tests, you have high confidence they'll work together — without actually running them together. This matters because integration testing at microservice scale is expensive: spinning up 10 services in CI to verify that two of them talk to each other is a significant infrastructure investment, and it's slow. Contract testing offers a faster verification path by testing the interface rather than the full behaviour.

Where it fits in the test pyramid

Contract tests sit between unit tests and integration tests. They're faster than integration tests (no full service mesh required) and slower than unit tests (they test at the API boundary, not inside a single function). The layer they specifically replace is the integration test that exists purely to verify that service A and service B can exchange data.

The framing that clarifies the value: contract tests verify interface compatibility, not behaviour correctness. Does service B still return the fields that service A expects? Does service B still accept the request shape that service A sends? Those are the contract test questions. Whether the business logic inside service B is correct is a separate concern, handled by service B's own unit and integration tests.

This boundary matters because contract testing is sometimes sold as replacing integration tests entirely. It doesn't — it replaces the narrow class of integration tests that test the data contract between services, not the behaviour behind the contract.

The classic case — and the classic mistake

The classic case for contract testing: microservices with independent deployment pipelines, owned by different teams, where integration tests are slow or impractical to run on every change.

Ten services, each with its own CI pipeline. When the user service renames a field from userId to id, the recommendation service that reads from the user service breaks. Without contract testing, you find this at the integration stage (slow, requires spinning up both services) or in production (expensive). With contract testing, the user service's CI pipeline runs the recommendation service's consumer tests and catches the incompatibility in minutes, without the recommendation service being deployed at all.

The classic mistake: adopting contract testing before you have the problem it solves.

I've read through numerous post-mortems and retrospectives from teams that built a Pact infrastructure for a monolith with two loosely coupled modules, or for a system with three services that can be spun up together with Docker Compose in 45 seconds. The overhead of contract testing infrastructure — writing consumer contracts, running a Pact broker, maintaining both consumer and producer test suites — has a real break-even point. If spinning up your dependent services in CI is fast and cheap, you probably haven't hit it.

The mistake is easy to make because the Pact documentation presents the model as universal good practice rather than as a solution to a specific scaling problem. It's worth being explicit: contract testing is a solution to integration testing at microservice scale. The tool is solving the problem of "we can't easily run all our services together." If you can run your services together easily, you probably don't need the tool.

Producer-driven vs consumer-driven contracts

This is the one architectural decision that determines most of the adoption pattern.

Consumer-driven contracts (CDC) — the approach Pact pioneered — put the consumer in charge of defining the contract. The service that calls the API writes a pact: "I need these fields, in this shape, with these constraints." The producer then runs the consumer's tests against its own implementation to verify it satisfies those requirements.

CDC makes intuitive sense for teams where services are genuinely independent: the consumer knows exactly what it needs, and the producer proves it can deliver. The Pact tool is built around this model — consumers publish pacts to a Pact Broker, producers pull them down and run them in CI. A producer that breaks a consumer's pact fails its own CI build before the breaking change is deployed.

Producer-driven contracts — sometimes called schema-driven or provider-driven — put the producer in charge. The producer defines the contract (often as an OpenAPI spec or JSON Schema), and consumers test against it. This is closer to the traditional API documentation model: the producer publishes the spec, consumers validate that they're using it correctly.

CDC catches breaking changes more reliably because the consumer explicitly states what it needs. Producer-driven contracts catch a different category of problem: consumers using the API in undocumented ways. Neither is universally better. The choice usually follows the organisational model — which team has the authority to define the interface?

When contract testing is overkill

The conditions that make contract testing worth the overhead:

  • Multiple services with genuinely independent deployment pipelines
  • Services owned by different teams who don't coordinate on deploys
  • APIs that change frequently enough that compatibility breaks are a recurring operational problem
  • CI infrastructure too slow or expensive to run full integration tests on every PR

If most of those conditions don't apply, simpler alternatives often cover the need with less investment:

Schema validation in CI — if you have an OpenAPI spec, tools like Dredd or schemathesis can run your spec against your live API and catch drift between the documented interface and the implemented interface. This catches the producer-side problem (API diverges from spec) without requiring consumer-side contract files.

Response snapshot tests — for simpler cases, snapshot-testing the response shape of key endpoints catches accidental field removal or type changes. No Pact broker required, no consumer pacts to maintain. It's a coarser check, but it's one that takes an afternoon to set up rather than a sprint.

Fast integration tests — if your services are containerised and can be spun up with Docker Compose, the full integration test may be cheaper than you think. A 90-second integration test that runs both services is often a better investment than a contract testing infrastructure for systems with two or three services.

The model is genuinely good. Understanding the interface contract between services is a real concern that gets more expensive to ignore as systems grow. But "contract testing" the ecosystem has become somewhat synonymous with "use Pact," and Pact is a specific tool solving a specific problem at a specific scale. Separating the model from the marketing lets you apply the right-sized solution. For most of what API test documentation covers, schema validation and response assertions are the practical starting point — contract testing infrastructure is the next rung up the ladder, not the first step.


// related

Deep dives·28 October 2025 · 9 min read

REST vs GraphQL testing: the actual differences

Most 'REST vs GraphQL' content is about API design. The testing perspective is different — query construction, schema-aware tooling, the N+1-shaped test bug, and why GraphQL flips the test pyramid.

api-testinggraphqlrestcomparison