Contract Testing for Microservices
- Contributor
- Apr 22
- 5 min read
In a microservices architecture, services depend on each other. The naive way to verify integration is to run them all together end-to-end. That works for two services. For twenty, it's a nightmare. Contract testing offers an alternative: verify each service against the agreed-upon contract, independently.
The Problem
Service A calls Service B. They agree on a contract — the API shape, status codes, response format. Over time, B's team changes B's API. They run their tests; everything passes. They deploy. A breaks.
The problem: B's tests verify B against B's own assumptions. They don't verify B against A's expectations.
End-to-end tests catch this but are slow and brittle. Contract tests catch it without the cost.
The Idea
Each consumer (A) declares the contract it expects from the provider (B). The provider verifies, in its own test suite, that it satisfies those contracts.
Two test runs:
Consumer test: A's tests run against a mock of B that returns what the contract says. Verifies A handles the contract correctly.
Provider test: B's tests run the contract against the real B. Verifies B produces what the contract promises.
If both pass, A and B can talk to each other without ever running together.
How It Works (with Pact, as Example)
A typical flow:
Consumer side (A): A's tests describe interactions with B
Pact generated: when A's tests run, they produce a contract file (a "pact")
Pact published: the file is uploaded to a Pact Broker
Provider side (B): B's tests pull the pact and verify B satisfies it
Both verified: if both pass, the integration works
pact.given('a user exists')
.uponReceiving('a request for the user')
.withRequest({ method: 'GET', path: '/users/123' })
.willRespondWith({
status: 200,
body: { id: 123, name: 'Sam' }
});
When Contract Testing Helps
Particularly valuable when:
Multiple services depend on the same provider. One change can break many consumers.
Services are deployed independently. No shared CI run guarantees compatibility.
Teams own different services. Coordination friction is high.
The system has too many services for end-to-end tests.
Less valuable when:
You have a monolith or just a few services. Direct integration testing is fine.
One team owns the whole stack. Coordination cost is low; direct testing works.
Contracts change rarely. The maintenance cost may exceed the benefit.
Consumer-Driven vs. Provider-Driven
Two flavors:
Consumer-driven contracts: the consumer specifies what it needs. The provider verifies it can deliver.
Provider-driven contracts: the provider specifies what it offers. Consumers verify they can handle it.
Consumer-driven is more common. The reasoning: the consumer's needs drive what the provider must deliver. Providers don't get to ship breaking changes consumers can't handle.
What to Put in the Contract
Contracts should specify:
HTTP method, path, query parameters
Request headers (auth, content-type)
Request body shape
Response status code
Response headers
Response body shape and types
What contracts shouldn't specify:
Specific values (use types and presence checks)
Internal implementation details
Performance characteristics (cover separately)
The contract is the interface, not the implementation.
The Pact Broker
A Pact Broker (or equivalent) is the shared infrastructure for contract testing:
Stores pacts published by consumers
Provides pacts to providers for verification
Tracks which versions are compatible
Powers "can-i-deploy" checks
The broker becomes the source of truth for service compatibility. Before deploying, check the broker: are my current versions compatible with what's deployed?
Verification Run Locations
Contracts can be verified:
In consumer CI: A's tests generate the pact every time
In provider CI: B's tests verify the pacts every time
On contract changes: when a pact changes, the affected provider verifies
The "can-i-deploy" check in deployment pipelines ties this together: a service deploys only if its contracts are satisfied.
What Goes Wrong
Contract drift. Provider changes behavior in a way that doesn't violate the contract but breaks the consumer anyway. The contract was too loose.
Mock divergence. Consumer's mocks don't match reality. Tests pass with mocks; production fails.
Stale pacts. Old consumer contracts not retired. Provider verifies against contracts no real consumer uses.
Over-specified contracts. Consumer pins details that aren't really requirements. Limits provider's flexibility for no benefit.
Under-specified contracts. Contract doesn't capture the actual usage. Provider can change things the contract didn't pin.
The contract has to capture what the consumer actually depends on, not more, not less.
Tools
Common contract testing tools:
Pact (most popular, multi-language)
Spring Cloud Contract (Java/Kotlin)
Postman / OpenAPI contracts (lighter weight)
OpenAPI / Swagger for schema-level contracts
Pact is the most mature for consumer-driven contracts. OpenAPI is good for documentation-based contracts that may be lighter-touch.
Schema-Level Contracts
A lighter form: just use OpenAPI/JSON Schema to define the API shape, verify both consumers and providers against the schema.
Producer publishes OpenAPI spec
Consumers generate clients from it
Tests verify the producer matches the spec
Less rigorous than full contract testing (doesn't capture consumer-specific expectations), but simpler. For many teams, this is enough.
Versioning
Contracts evolve. Versioning is essential.
A working pattern:
Each consumer pact is tagged with the consumer version
Providers verify against pacts from all currently-deployed consumer versions
Old pacts are retired when their consumer versions are no longer deployed
Without versioning, the provider doesn't know which contracts still matter.
Trade-Offs
Contract testing has costs:
Setup overhead (broker, tooling, training)
Maintenance of contracts as APIs evolve
Discipline to keep contracts focused on actual dependencies
Benefits:
Independent deployment of services
Fast feedback when changes would break integration
No need for full end-to-end test environments
For mature microservices architectures, the trade is usually worth it. For smaller systems, the overhead may exceed the value.
A Working Implementation
For a team adopting contract testing:
Pick one consumer-provider pair. Start small.
Define the contract in consumer tests.
Set up a broker (Pact Broker is free for small use).
Have provider verify in its CI.
Expand to other pairs as the pattern is proven.
Don't try to retrofit contracts onto every service at once. Adoption is gradual.
When to Use End-to-End Tests Instead
Contract testing complements but doesn't replace end-to-end tests entirely. E2E still has value for:
The most critical user journeys
Verifying configuration and deployment are correct
Catching issues that span the contract boundary
The right mix: contract testing for service-pair integrations, E2E for system-level verification of critical flows.
Key Takeaway
Contract testing verifies that services can talk to each other without running them together. Consumers declare what they expect; providers verify they deliver. Particularly valuable in mature microservices architectures where end-to-end testing has become impractical. Use a contract broker to coordinate; version contracts to evolve safely. Keep contracts focused on actual dependencies — over- or under-specifying both cause problems. The combination of contract testing and selective E2E is the working approach for most multi-service systems.
Related reading
Keep learning. This article is part of the Test Automation path in the ShiftQuality Learning Center. Build test automation that lasts, with ROI you can defend.


