Contract Testing: Catching Breakage Before Clients See It
A contract is a promise. Contract testing keeps you honest. Here's how to do it right.
What a contract is
A contract is the published agreement between an API producer and its consumers: what endpoints exist, what inputs they take, what outputs they return, and under what conditions.
Contracts live in concrete artifacts:
- REST: OpenAPI / Swagger specification.
- GraphQL: SDL schema.
- SOAP: WSDL + XSD.
- gRPC:
.protofiles. - Async: AsyncAPI for events.
Contract testing verifies that the implementation matches the contract. The contract is the source of truth; the implementation should never silently drift from it.
Three flavors of contract testing
1. Schema-based (provider-side)
Run the real server, hit its endpoints, and validate responses against the published schema.
┌──────────┐ real requests ┌────────┐
│ Test │ ────────────────> │ Server │
│ runner │ │ │
│ │ <──── JSON ────── │ │
└──────────┘ └────────┘
│
│ validate against openapi.yaml
▼
✓ or ✗
Cheap, fast, catches 80% of drift. Tools: Dredd, Schemathesis, Postman, ShiftLeft.
2. Consumer-driven (Pact-style)
Consumers publish expectations about what they need from the producer. The producer verifies they can satisfy each consumer.
┌──────────┐ ┌──────────┐
│ Consumer │ ── publishes "pact" expectations ────> │ Broker │
│ │ │ │
└──────────┘ └─────┬────┘
│
│ consumed during CI
▼
┌──────────┐
│ Producer │ ── verifies
└──────────┘
Stronger, more expensive. Good for microservice fleets where many teams own different consumers. Tools: Pact, Spring Cloud Contract.
3. Snapshot-based
Record live responses; compare against the snapshot on every run. Fail if anything changed unexpectedly.
real server → capture JSON → commit to repo → compare next run
Quick to set up. Brittle (any change fails). Good for complex responses where the schema is vague.
Most mature teams combine all three: schema tests in CI for every build, Pact verification for inter-service contracts, snapshots for visual/complex responses.
What schema tests should cover
For every endpoint in the spec:
- Every documented status code is reachable with an appropriate request.
- Response body matches the schema for that status.
- Required fields are always present.
- Nullable fields are
nullor correct type. - Enums only contain values the spec declares.
- Nested objects match their referenced schemas.
- Response content-type matches the spec.
- Documented headers are present (
Locationon 201,ETagwhen required, etc.).
Tools can derive all of this from an OpenAPI file. What tools can't do:
- Decide what status code a specific test should trigger (you write the request).
- Know which business-rule combinations matter.
- Generate realistic auth tokens.
Contract testing + hand-written business tests = full coverage.
Breaking vs non-breaking changes
Every contract change falls into one of three buckets:
| Change | Breaking? | Action |
|---|---|---|
| Add a new endpoint | No | Safe to ship |
| Add an optional field to a response | No | Safe |
| Remove a field from a response | Yes | Deprecate first, remove later |
| Rename a field | Yes | Add new + deprecate old, remove in next major |
| Make an optional request field required | Yes | Existing clients will break |
| Make a required request field optional | No | Safe |
| Change a field's type | Yes | Major version bump |
| Add a new required response header | No | Safe for clients who ignore headers |
| Add a new enum value | Maybe | Safe if consumers validate loosely; breaking if strictly |
| Remove an enum value | Yes | Breaking |
| Change a default value | Yes | Silent behavior change |
A good contract test run classifies changes automatically. Tools: openapi-diff, graphql-inspector, pact-broker's "can-i-deploy".
The CI pipeline shape
For an API with OpenAPI:
- Pull request opens.
- CI runs the full schema-test suite against the branch.
- CI computes the diff between the branch's OpenAPI file and
main's. - Classifier marks changes as safe/dangerous/breaking.
- Breaking changes require an explicit override label on the PR (e.g.,
@breaking-change-ok). - On merge, the OpenAPI file is published to a registry.
- Downstream consumer repos are notified of the new version.
This turns contract drift from a gradual decay into a discrete, reviewable event.
Contract tests for async APIs
REST and GraphQL aren't the only contracts. Event-driven APIs (Kafka, RabbitMQ, SNS) also need contract testing:
- Schema registry (Confluent Schema Registry, Apicurio) stores the schema of each event.
- Producers register a schema version when publishing.
- Consumers declare the schema version they understand.
- Compatibility rules (backward, forward, full) enforce what changes are allowed.
AsyncAPI is the growing standard for documenting event-driven contracts. Tools are maturing; expect contract testing for events to be as mainstream as OpenAPI testing by 2027.
Common mistakes
1. "We'll write the OpenAPI later." Contract tests need a contract. If the spec is written after the code, the spec describes the bug, not the intent.
2. Generating the spec from code. Tempting, but means every code change is "automatically" a contract change. The contract should be the spec; the code should conform. Pick one side as authoritative.
3. Trusting the happy path. Contract tests that only cover 200 responses miss half the contract. Every declared error response needs a test too.
4. No diff-based gating in CI. If a breaking change can sneak into main, the whole apparatus fails.
5. Pact without a broker. Pact shines when consumer expectations are centralized. Without a broker, you're just running extra tests for no reason.
6. Snapshot tests that update on every PR. Developers get in the habit of snapshot --update any time a test fails. Snapshots become documentation of the latest whim, not an actual assertion.
What to automate vs what to write by hand
Automate (from OpenAPI / WSDL / SDL):
- Schema validation on every documented response.
- Negative tests per field.
- Status-code reachability.
- Breaking-change diff on every PR.
Write by hand:
- Business-rule scenarios that cross multiple endpoints.
- Flows (login → do-thing → log out).
- Real-world edge cases specific to your domain.
ShiftLeft's contract-testing workflow reads your OpenAPI, generates the automatable tests, and leaves the business scenarios for you to add on top. The split is deliberate — the stuff that can be generated shouldn't consume human time.
What's next
You've completed the Testing cluster. Next up: AI-assisted testing — how modern AI changes test generation from template-filling to genuinely useful automation.
Related lessons
Most API bugs live in input validation. Here's how to test it systematically.
The network is unreliable. Here's how clients should retry, how servers should behave, and how to test both.
Happy paths prove your API works. Negative paths prove it doesn't break. Both matter.
Read more on the blog
Contract testing for microservices prevents the integration failures that plague distributed systems by formally verifying that services honor the agreements their consumers depend on. This practical guide covers consumer-driven contracts, provider verification, the Pact framework, and how OpenAPI-based tools like Shift-Left API make contract testing accessible without specialized expertise.
API schema validation drift detection is the automated process of comparing live API responses against the documented OpenAPI specification to identify undocumented changes in field types, required properties, response formats, and nested structures. When drift goes undetected, it silently breaks API consumers and causes production integration failures.