API Contract Testing: Tools, Examples & CI Setup
Abhay khant
Jan 1, 1970 • 11 min read
Your frontend team ships a breaking change to the user payload. Your mobile team still expects the old shape. Neither realizes until production shows a blank screen. API contract testing catches that mismatch before deploy, by checking both sides of an API against a shared agreement instead of hoping they stay in sync.
Contract testing treats the API as a two-party agreement: the provider (the service exposing the API) and the consumer (the app calling it). Each side tests against the same contract file, so a change on either side fails fast instead of failing in production. It is not a replacement for integration tests — it is a faster, narrower check that runs on every pull request.
This guide covers how consumer-driven contracts work, when to use Pact versus OpenAPI schema validation, how to wire contract tests into CI/CD, and what contract testing deliberately does not cover.
What is contract testing?
A contract is a machine-readable description of an API interaction: the request shape a consumer sends, the response shape the provider returns, and any headers or status codes in between. Both teams test against that description independently, so each side can catch a mismatch without deploying the other.
Contract testing sits between unit tests and full integration tests. Unit tests check one component in isolation. Integration tests spin up real services and check they talk to each other. Contract tests check that each side honors the shared agreement, using mocks instead of live dependencies. That makes them fast, deterministic, and runnable on every pull request — the three things that let a contract test actually live in your pipeline.
Contract testing vs integration testing
The question teams ask most is how contract testing differs from integration testing. The short answer: integration tests verify that two running services work together; contract tests verify that each side would work with the other, before either one is deployed.
| Contract testing | Integration testing | |
|---|---|---|
| What it checks | Each side honors the shared contract | Real services interoperate |
| Dependencies | Mocks/stubs of the other side | Live instances |
| Speed | Milliseconds | Seconds to minutes |
| Where it runs | Every pull request | Staging, pre-release |
| Failure tells you | Which side broke the agreement | Something broke, dig deeper |
The practical rule: run contract tests continuously, run integration tests before release. A contract test failing on a PR points at the exact breaking field. An integration test failing in staging tells you the systems don't work together — then you still have to find out why.
How consumer-driven contracts work
Consumer-driven contract testing inverts the usual flow. Instead of the provider publishing a spec and consumers hoping it stays accurate, each consumer captures its actual expectations — the requests it really sends and the responses it really handles — into contract files. The provider then verifies every consumer contract against its implementation.
The flow looks like this:
- The consumer records the interactions it depends on (usually from its own test suite).
- Those interactions become a contract file, committed alongside the consumer's code.
- The consumer runs its side of the test against the contract locally.
- The contract is published to a broker or registry.
- The provider pulls the contract, runs its own tests against it, and either passes or finds drift.
- On a breaking change, the provider sees which consumers would be affected before merging.
This catches drift at the source. When a consumer's expectations and a provider's implementation disagree, the provider's verification step fails — while the change is still in development, not after deploy. Martin Fowler's consumer-driven contracts article is the canonical explanation of this pattern.
The trade-off is real: consumer-driven testing shines when you control both sides. It does less for public APIs with unknown consumers, where the contract is your OpenAPI spec and the verification is schema validation against it.
Pact vs OpenAPI schema validation
Teams usually end up choosing between two approaches: Pact for consumer-driven contract testing between services you control, and OpenAPI schema validation when the API itself — usually the spec — is the source of truth.
Pact (and its documentation) records interactions from the consumer's test suite into Pact files, then has the provider verify them. It excels at finding drift on the exact interactions consumers depend on, and its broker gives you a published history of which consumer versions a provider change would break. The cost: it is a workflow, not a library. Teams adopt Pact conventions, brokers, and CI stages.
OpenAPI schema validation works the other way: the spec is the contract, and both sides validate against it. Tools like Schemathesis generate property-based tests from the schema, sending thousands of automatically-derived requests to find cases the spec didn't anticipate. Dredd compiles the API description into executable tests directly. This approach is simpler to adopt — if you already have an OpenAPI spec, you are most of the way there — but it verifies conformance to the spec, not to what consumers actually do. A consumer that relies on an undocumented field will pass schema validation and break in production.
Which should you pick?
- You control both sides, many consumers, breaking changes are frequent → Pact. The broker tells you exactly which consumers break.
- Public or third-party API, spec is the source of truth → OpenAPI schema validation with Schemathesis or Dredd.
- Polyglot microservices on the JVM → Spring Cloud Contract generates provider stubs and consumer tests from a shared contract.
- Small team, existing spec, want the fastest win → Start with schema validation; add Pact-style consumer contracts where drift actually hurts.
The two approaches are not mutually exclusive. Many teams run Schemathesis against their OpenAPI spec continuously and Pact contracts for the handful of consumers that matter most.
Contract testing in CI/CD
Contract tests earn their keep only when they run automatically — and automation is the norm: 75% of API teams run CI/CD pipelines, per Postman's State of the API 2025 report. The pattern: consumers run their side on every push, the provider verifies all consumer contracts on every push, and the broker or registry gates merges when verification fails.
A minimal Pact CI setup on the consumer side, using the Pact CLI to publish verified contracts:
name: Consumer Contract Tests
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test # runs consumer tests that record Pact interactions
- run: npx pact-broker publish ./pacts \
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--broker-token ${{ secrets.PACT_BROKER_TOKEN }} \
--consumer-app-version ${{ github.sha }}
On the provider side, the verification step pulls every consumer contract and fails the build if any of them no longer matches:
name: Provider Verify Contracts
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run start:provider & sleep 5
- run: npx pact verify \
--pact-broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--pact-broker-token ${{ secrets.PACT_BROKER_TOKEN }} \
--provider-app-version ${{ github.sha }}
For schema-driven validation, the gate is simpler. Schemathesis runs its generated test suite against the running API on every push:
name: Schema Validation
on: [pull_request]
jobs:
schemathesis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run start:api & sleep 5
- run: schemathesis run http://localhost:3000/openapi.json
The same commands slot into GitLab CI or any other runner — the pattern is what matters: both sides test against the shared agreement, on every change, before merge. Test your OpenAPI spec and a sample payload in the ToolSura API tester or validate the schema itself with the ToolSura JSON validator before wiring it into a pipeline.
Do you still need integration tests?
Yes. Contract tests cover the shape of interactions, not their behavior. They will not catch a service that responds correctly but writes the wrong row to the database, a timeout under load, or an auth flow that only fails with real credentials. Keep integration tests at the release boundary — contract tests reduce how often you need them, they do not replace them. For a deeper look at where schema checks fit in a broader testing strategy, see our JSON Schema generator guide.
What contract testing does NOT cover
Being explicit about the boundaries saves you from a false sense of safety. Contract testing does not verify:
- Behavior and side effects. A response that matches the contract can still do the wrong thing. Contract tests check shape, not correctness.
- Performance and load. A contract test proves the interaction fits the agreement, not that it meets a latency budget.
- Auth and authorization. Security rules rarely show up in a contract file, and mocking them hides exactly the failures you want to see.
- Stateful or sequential edge cases. Most contract tools assume one-shot request/response interactions. Multi-step flows, retries, and idempotency are hard to express.
- Cross-service behavior. Contract tests never deploy both sides. Anything that only fails when two real services interact needs an integration test.
- Message queues and events. Classic contract testing is HTTP-centric. For asynchronous events, look at Pact's message support (Pact files can describe messages, not just HTTP) or schema registries that validate event payloads — but know that the guarantees are weaker than for request/response.
Building contracts from OpenAPI schemas
When the spec is your source of truth, you generate contracts from it instead of recording them. OpenAPI 3.0 defines components that can be lifted into standalone contract files, and validation tools consume them directly. Keep the OpenAPI spec canonical and derive what you need from it, rather than maintaining a second copy by hand — that is how contract drift sneaks back in.
A minimal contract extracted from an OpenAPI component might look like — note the $schema key pointing at the JSON Schema specification:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "CreateUserRequest",
"type": "object",
"required": ["email", "name"],
"properties": {
"email": { "type": "string", "format": "email" },
"name": { "type": "string", "minLength": 1, "maxLength": 100 },
"roles": {
"type": "array",
"items": { "enum": ["admin", "user", "viewer"] }
}
}
}
Validate the schema and test payloads against it with the ToolSura JSON validator before wiring it into CI. If you are generating schemas from TypeScript types, our Zod to JSON Schema guide shows how to keep the contract in lockstep with the code that implements it.
Common mistakes
- Testing only the happy path. Contracts that omit error responses and status codes let the 400s and 500s drift silently. Include them.
- Letting the provider own the contract. When the provider writes the contract, it describes what it happens to return — not what consumers depend on. Consumer-driven means consumer-recorded.
- Maintaining a second contract by hand. Generate contracts from your OpenAPI spec or your types. A hand-edited copy is drift waiting to happen.
- Treating contract tests as a compliance checkbox. A contract test that never runs in CI is documentation, not protection. It must gate merges to be useful.
- Ignoring the broker's can-i-deploy. Pact's broker answers "can I safely deploy this provider version?" — teams that skip it lose the whole point of recording consumer versions.
Related reading
- JSON Schema generator guide — where schemas come from, and the CI gate pattern
- Zod to JSON Schema — keeping contracts in lockstep with TypeScript code
- Quicktype JSON schema inference — deriving schemas from sample payloads
- TypeScript type safety guide — the compile-time side of the contract
- Test payloads against live endpoints with the ToolSura API tester
- Validate schemas and payloads with the ToolSura JSON validator
References
- Pact consumer-driven contract testing docs — https://docs.pact.io/
- Martin Fowler, Consumer Driven Contracts — https://martinfowler.com/articles/consumerDrivenContracts.html
- Schemathesis — property-based testing for your API — https://schemathesis.readthedocs.io/
- Dredd — HTTP API testing framework — https://dredd.org/
- Spring Cloud Contract reference — https://docs.spring.io/spring-cloud-contract/docs/current/reference/html/
- Postman State of the API 2025 — https://www.postman.com/state-of-api/
- JSON Schema specification — https://json-schema.org/
- OpenAPI 3.0 specification — https://spec.openapis.org/oas/v3.0.3.html


