Contract Testing for Microservices: Consumer, Provider, and CI
Catch breaking service changes before deployment with consumer-driven contracts, provider verification, state fixtures, compatibility gates, and schema tests.
Contract tests verify that a provider still satisfies the requests and response fields its consumers depend on. Consumers publish minimal interaction contracts; providers replay them against the real implementation in CI; a compatibility gate prevents either side from deploying an incompatible version. They complement schema, unit, integration, and end-to-end tests.
Steps at a glance
- Inventory the real interaction. Trace the consumer code to identify the exact request, status handling, fields, types, and enum values it depends on.
- Write the minimal consumer contract. Run the consumer against a contract mock and assert only its real dependencies with flexible matchers.
- Publish and version the contract. Associate the contract with the consumer version and environment or branch so providers can verify the relevant compatibility matrix.
- Verify against the provider. Replay contracts against the real provider implementation with deterministic provider-state setup in provider CI.
- Gate deployment and evolve safely. Block incompatible provider or consumer versions, then use additive changes and staged removal for breaking evolution.
Key takeaways
- The consumer owns its expectations because only the consumer knows which fields, types, statuses, and behaviors it actually uses.
- Contracts should assert the minimum dependency with flexible matchers, not freeze entire example responses.
- Provider verification runs contracts against the real provider with deterministic provider-state fixtures in the provider's CI.
- Version and tag contracts by consumer build or environment, then block deployments whose consumer-provider matrix is incompatible.
- Use schema tests for broad interface shape, contract tests for real consumer usage, and a small end-to-end suite for deployed-system behavior.
Consumer-driven contract tests prove that an API provider still satisfies the behavior its consumers use. They move cross-service compatibility checks out of a slow shared environment and into each service's CI, where a breaking response or request change can fail before deployment.
The consumer is the source of expectations because the provider cannot know which details every client depends on. One client may read only id and status; another may branch on a specific error code. A complete API schema describes possibilities. A consumer contract records actual dependency.
Where contract tests fit
A balanced service test strategy has several layers:
| Layer | Proves | Does not prove |
|---|---|---|
| Unit | Local business logic | Real interface wiring |
| Schema | Requests and responses conform to an API specification | A deployed consumer is compatible |
| Consumer contract | Provider satisfies one consumer's used interaction | Infrastructure and full workflows work |
| Provider component | Provider works with database and close dependencies | Real multi-service deployment works |
| End-to-end | Critical journey works in a deployed system | Fast, exhaustive compatibility coverage |
Contract tests reduce the number of cross-service failures that reach end-to-end tests. They do not remove the need for a small suite that checks DNS, authentication, deployment configuration, queues, and other real infrastructure.
The consumer writes the interaction
Suppose checkout calls an order service:
GET /orders/ord_42The response may contain twenty fields, but checkout reads only:
{
"id": "ord_42",
"total": 4250,
"status": "open"
}The contract should assert:
- the method and path
- request body and meaningful headers
- response status
idis a stringtotalis a number in the expected unitstatusbelongs to the values checkout handles
It should not assert exact timestamps, ordering of unrelated fields, internal headers, or the full provider response. Over-specified contracts turn harmless provider evolution into noise, teaching teams to ignore failures.
The consumer test runs its real client code against a contract mock. That proves the published contract matches what the consumer actually sends and parses rather than a document someone wrote separately.
Use semantic matchers
Prefer matchers that express dependency:
- type: string, integer, boolean, array, object
- required field presence
- enum or regular-expression constraint
- minimum array length when the consumer requires data
- unordered collection when order is irrelevant
- exact value only when the consumer truly branches on it
An example value makes a contract readable; the matcher states what may vary. Distinguish “looks like the example” from “must equal the example.”
Avoid the opposite failure too: a contract that says only “returns 200 and an object” protects nothing. If the consumer reads a field or makes a decision from it, encode that dependency.
Provider states make verification deterministic
Each interaction needs a named provider state such as:
order ord_42 exists and is opencustomer has no active subscriptioninventory is unavailable
During provider verification, the harness asks the provider test environment to establish that state, then replays the consumer request against the real handler.
Provider states are setup hooks, not alternate provider behavior. They may seed a database, stub a third-party dependency, or select a fixture. Keep them deterministic, isolated, and idempotent so verification can run in parallel and in any order.
Test success and meaningful failure contracts. Consumers often depend more strongly on error status, machine-readable error codes, and retry semantics than on the happy response.
Publish contracts as versioned artifacts
A contract belongs to a specific consumer build. Publish it to a broker or another versioned store with:
- consumer and provider names
- consumer commit or build version
- branch or environment tag
- verification results by provider version
- deployment status when available
Do not overwrite one latest.json file. Compatibility is a matrix: provider version P may satisfy consumer versions A and B but not C. Deployment gates need that history to answer whether the exact versions entering an environment can coexist.
For small systems, contracts can live in repositories, but a broker becomes valuable when many consumers, versions, and independent deployment pipelines must coordinate.
Verify in provider CI
When a provider changes, its pipeline should fetch relevant contracts and replay them against the real provider implementation. A failure reports:
- the consumer and interaction
- expected request or response condition
- actual result
- provider state
- versions being compared
Make verification a required check. A dashboard that says “incompatible” after the provider deploys is monitoring, not prevention.
Run verification for the consumers currently deployed and those preparing to deploy. Verifying every historical contract forever can prevent intentional cleanup long after old consumers disappear.
Evolve interfaces with both sides visible
For a breaking field change:
- Provider adds the new field while preserving the old one.
- Consumer publishes a contract using the new field.
- Provider verifies both old and new contracts.
- Updated consumer deploys and old versions retire.
- Provider removes the old field only when the compatibility matrix shows no active consumer needs it.
This is expand-and-contract applied to interfaces. Contract history tells you when contraction is safe.
Provider teams should not edit consumer contracts to make a provider build pass. If the expectation is wrong, change it through the consumer's code and tests so the artifact remains tied to actual usage.
Avoid common contract-test traps
- Testing provider implementation details: Contracts describe observable boundary behavior.
- Snapshotting whole responses: Match only dependencies.
- Mocking the provider during provider verification: Verify the real handler and serialization layer.
- Non-deterministic provider states: Seed explicit data and isolate tests.
- Ignoring asynchronous messages: Events and queues need message contracts for payload, key, headers, ordering assumptions, and evolution.
- Skipping the deployment gate: Verification without enforcement still allows incompatible releases.
- Treating contracts as business-logic tests: Keep provider behavior coverage in its own test suite.
Start with one painful boundary
Choose an integration that often breaks in staging or forces teams to coordinate deployments. Add contracts for the consumer's critical success and error paths, provider verification in CI, and a compatibility gate. Measure whether cross-service defects move earlier and whether the end-to-end suite can shrink.
TIP
A good contract is intentionally incomplete: it protects everything this consumer needs and permits everything it does not care about.
Use the Contract Test Designer to generate consumer and provider setups for the repository's framework, and pair it with the OpenAPI Doc Writer so implementation, public schema, and real consumer expectations converge.
Frequently asked questions
- What is consumer-driven contract testing?
- It is a testing method where each API consumer records the requests it sends and the response elements it relies on. The provider verifies those contracts against its real implementation before deployment, catching consumer-breaking changes without a shared end-to-end environment.
- Are contract tests the same as OpenAPI validation?
- No. OpenAPI validates a provider-defined schema and can describe the whole API. Consumer contracts capture the narrower behavior each real consumer uses, including specific interactions and provider states. Use both: schema conformance for the public interface and contracts for compatibility with deployed consumers.
- Do contract tests replace integration tests?
- No. They verify the boundary between services. Unit tests still own business logic, component or integration tests own the provider with its database and dependencies, and a small end-to-end suite verifies deployment, networking, identity, and critical journeys.
- Why should contracts use matchers instead of exact JSON?
- Exact example matching over-specifies irrelevant values and makes harmless provider changes fail. Match the fields and constraints the consumer uses—types, required keys, enums, and status—while allowing unrelated fields and values to evolve.
Related
- Contract Test DesignerDesign consumer-driven contract tests between services so an API provider can't break its consumers unnoticed — without slow, flaky full end-to-end environments. Use when independent services or teams integrate over an API, when integration bugs only surface in staging or prod, or when E2E suites are too slow and brittle to catch breaking API changes.
- Integration Test DesignerDesign integration tests that exercise components against REAL collaborators — actual database, queue, HTTP boundary — at a deliberately chosen seam, instead of a unit suite that mocks everything or a slow flaky full E2E. Use when bugs slip past green unit tests, when wiring or contracts between layers break in production, or when a mocked DB test passes but the real query/migration/serialization fails.
- OpenAPI Doc WriterProduce and maintain OpenAPI documentation for an HTTP API. Use when documenting endpoints, request/response schemas, or generating API reference docs.
- API ArchitectUse this agent to design APIs — resource modeling, versioning, pagination, error contracts, REST vs GraphQL. Examples — designing a public API, reviewing an API spec, planning a breaking change.
- Test EngineerUse this agent to write and improve automated tests — unit, integration, and edge cases. Examples — adding coverage to an untested module, writing regression tests for a bug, designing a test plan.
- Test ScaffolderScaffold a test file with sensible cases for a given module or function. Use when adding tests to untested code and you want a fast, structured starting point.
- Coverage Gap FinderRun the project's coverage tool and identify the highest-value untested paths — error branches, edge cases, and critical modules — then propose specific test cases for each gap. Use when you have a coverage report but don't know where new tests will pay off most.