Contract Testing skill for Claude Code
Catch breaking API changes before either side deploys. Claude picks between consumer-driven Pact contracts and provider-owned OpenAPI checks based on who the consumers are, then wires the full loop: consumer tests with type matchers, provider verification with named states, a broker, can-i-deploy before every deploy and record-deployment after. Breaking API changes fail a pipeline instead of a production release.
- name
contract-testing - size182 lines · ≈1,850 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Sets up contract tests between services so API changes that break a consumer fail in CI before deploy. Covers consumer-driven contracts with Pact (consumer tests, provider verification, provider states, broker, can-i-deploy) and schema-based contracts with OpenAPI (response validation, breaking-change diffs with oasdiff, Schemathesis). Use when two services or a frontend and API deploy independently, when a provider change broke a consumer in production, when replacing a shared staging environment as the integration gate, or when choosing between Pact and OpenAPI validation. Try asking
- Add Pact contract tests between our React app and the orders API
- A backend rename broke the mobile app last week. How do we stop that in CI?
- Fail our PRs when someone makes a breaking change to openapi.yaml
Use it when
- Services or a frontend and API deploy independently
- A provider change broke a consumer in production
- Replacing a shared staging environment as the integration gate
- Deciding between Pact and OpenAPI-based validation
Skip it when
- Both sides ship in one deploy unit: integration tests are enough
- You are designing the API resources and errors themselves: use rest-api-design
How to check the result
Renaming a field a consumer reads makes provider verification and can-i-deploy fail, while adding an unused field passes everywhere.
The common mistake it prevents
Skipping record-deployment, so the broker never knows what is in production and the deploy gate silently checks nothing.
A contract test proves that what a consumer expects and what a provider delivers still agree, without deploying both together. Done well, a provider cannot ship a change that breaks a deployed consumer, and each side tests in isolation in seconds.
Pick the approach
| Situation | Approach |
|---|---|
| You own both sides; several internal consumers with different needs | Consumer-driven contracts (Pact) |
| Public or partner API with unknown consumers | Provider-owned OpenAPI spec + breaking-change diff + spec-conformance tests |
| Provider already has an accurate OpenAPI spec, consumers use Pact | Bi-directional (PactFlow) or validate consumer pacts against the spec |
| Async messages (Kafka, SQS events) | Pact message contracts, or a schema registry with compatibility rules |
| One team, one deploy unit | Not needed; use integration tests |
Contracts test the shape and semantics of the interface, not business logic. Keep provider business rules in the provider’s own tests.
Workflow (Pact)
- [ ] 1. Identify consumer/provider pairs and who deploys independently
- [ ] 2. Write consumer tests against the Pact mock server
- [ ] 3. Publish pacts to a broker tagged with version and branch
- [ ] 4. Provider verifies pacts with state handlers
- [ ] 5. Gate deploys with can-i-deploy; record deployments
- [ ] 6. Add webhook so pact changes trigger provider verification
2. Consumer test (pact-js, V4 API)
import path from 'node:path';
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
import { OrdersClient } from '../src/orders-client';
const { integer, string, eachLike, datetime } = MatchersV3;
const pact = new PactV4({ consumer: 'web-app', provider: 'orders-api', dir: path.resolve('pacts') });
describe('OrdersClient', () => {
it('fetches an existing order', async () => {
await pact
.addInteraction()
.given('order 42 exists')
.uponReceiving('a request for order 42')
.withRequest('GET', '/orders/42', (b) => b.headers({ Accept: 'application/json' }))
.willRespondWith(200, (b) =>
b.jsonBody({
id: integer(42),
status: string('paid'),
createdAt: datetime("yyyy-MM-dd'T'HH:mm:ssX", '2026-01-05T10:00:00Z'),
items: eachLike({ sku: string('A-1'), qty: integer(1) }),
}))
.executeTest(async (mock) => {
const order = await new OrdersClient(mock.url).getOrder(42);
expect(order.status).toBe('paid');
});
});
});
Consumer rules:
- Exercise the real client code, not raw
fetch, so the pact reflects actual usage. - Use type matchers (
integer,string,eachLike) rather than exact values, so the provider can return any valid data. - Include only fields the consumer actually reads. Extra provider fields are fine; a pact that lists every field blocks harmless provider changes.
- Cover the error responses the consumer handles (404, 422), each with its own state.
- Provider state names are a shared vocabulary:
order 42 exists, notsetup1.
Other languages have equivalent DSLs (pact-jvm, pact-python, pact-go, pact-net); follow the same structure.
3. Publish
pact-broker publish ./pacts \
--consumer-app-version "$GIT_SHA" \
--branch "$GIT_BRANCH" \
--broker-base-url "$PACT_BROKER_BASE_URL" \
--broker-token "$PACT_BROKER_TOKEN"
The CLI ships in the pactfoundation/pact-cli Docker image and the
@pact-foundation/pact-cli npm package. Always version by commit SHA, never by a
semver that repeats across commits.
4. Provider verification
import { Verifier } from '@pact-foundation/pact';
await new Verifier({
provider: 'orders-api',
providerBaseUrl: 'http://localhost:8080',
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
providerVersion: process.env.GIT_SHA,
providerVersionBranch: process.env.GIT_BRANCH,
publishVerificationResult: process.env.CI === 'true',
consumerVersionSelectors: [{ mainBranch: true }, { deployedOrReleased: true }, { matchingBranch: true }],
enablePending: true,
stateHandlers: {
'order 42 exists': async () => { await db.orders.insert(orderFixture({ id: 42, status: 'paid' })); },
},
}).verifyProvider();
- Run the real provider app with real routing and serialization; stub only its own downstream dependencies.
- State handlers set up data directly (DB insert or repository fake), not via other APIs.
enablePending: truelets new consumer expectations fail without breaking the provider build until the provider has passed them once.- Only publish verification results from CI.
5. Deploy gate
pact-broker can-i-deploy --pacticipant web-app --version "$GIT_SHA" \
--to-environment production --retry-while-unknown 6 --retry-interval 10
# after a successful deploy:
pact-broker record-deployment --pacticipant web-app --version "$GIT_SHA" --environment production
Both consumer and provider pipelines run can-i-deploy before deploying and
record-deployment after. Without record-deployment, the broker cannot know what is in
production and the deployedOrReleased selector verifies nothing.
Broker setup, webhooks, and the full CI flow are in references/pact-broker-flow.md; read it when wiring pipelines for the first time.
Workflow (OpenAPI schema contracts)
- [ ] 1. Make the spec the source of truth (generated from code or code generated from spec)
- [ ] 2. Lint the spec (Spectral or Redocly)
- [ ] 3. Validate real responses against the spec in provider tests
- [ ] 4. Fail PRs on breaking spec changes with oasdiff
- [ ] 5. Fuzz the running API against the spec with Schemathesis
# breaking-change gate in CI (base spec from main, head spec from the PR)
oasdiff breaking main-openapi.yaml openapi.yaml --fail-on ERR
# property-based conformance against a running instance
schemathesis run http://localhost:8080/openapi.json
In provider tests, assert each response conforms to the spec, e.g. with
jest-openapi (expect(res).toSatisfyApiSpec()) in JS or openapi-core in Python.
A spec that is not checked against real responses drifts within weeks.
Gotchas
- Contracts written by the provider team “for” consumers are not consumer-driven; they test what the provider thinks is used.
- Exact-value matching in pacts creates failures on any data change. Use matchers.
- Sharing one provider state that sets up everything makes states order-dependent and slow.
- Pact verifies HTTP shape, not auth flows; stub auth in verification with a known token or a request filter, and test auth separately.
- Breaking changes that are schema-compatible still break: changing the meaning of a field, new enum values the consumer does not handle. Treat new enum values as breaking unless consumers are documented to ignore unknowns.
can-i-deploywithout--to-environment(or with stale tags) checks the wrong matrix.
Verification
- Introduce a deliberate break in the provider (rename a field a consumer reads) and
confirm provider verification fails and
can-i-deployreturns non-zero. - Add an unused field in the provider and confirm nothing fails.
- For OpenAPI, delete a required response property in the spec on a branch and confirm
oasdiff breakingfails the PR.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/pact-broker-flow.md
# Pact broker and CI flow
Read this when wiring Pact into pipelines for the first time or debugging why
`can-i-deploy` says no.
## Broker options
- Self-hosted Pact Broker: Docker image `pactfoundation/pact-broker` with Postgres.
Protect it with basic auth or put it behind SSO; it holds API shapes, not secrets, but
still should not be public.
- Hosted: PactFlow (adds bi-directional contracts and a UI for teams).
## The full loop
```
Consumer PR Provider PR
----------- -----------
unit + pact tests unit tests
publish pacts (sha, branch) verify pacts:
can-i-deploy --to-environment selectors mainBranch, deployedOrReleased,
(waits for provider verification) matchingBranch; publish results (CI only)
deploy can-i-deploy --to-environment
record-deployment deploy
record-deployment
```
Webhook: configure a broker webhook on the `contract_requiring_verification_published`
event that triggers the provider's verification job with the pact URL and the provider
version/branch to verify. This lets a consumer PR get its verification result without
waiting for an unrelated provider build.
## Consumer version selectors, explained
| Selector | Verifies |
|---|---|
| `{ mainBranch: true }` | Latest pact from each consumer's main branch |
| `{ deployedOrReleased: true }` | Pacts for consumer versions currently recorded in any environment |
| `{ matchingBranch: true }` | Consumer pact from a branch with the same name as the provider branch (coordinated feature work) |
Use all three on provider builds. Add `includeWipPactsSince: '<date>'` with
`enablePending: true` to verify work-in-progress consumer pacts without failing the build.
## Reading a can-i-deploy failure
The output is a matrix of consumer version x provider version with a verification
result. Common causes:
| Output says | Meaning | Fix |
|---|---|---|
| No verification result | Provider has not verified this pact yet | Trigger provider verification (webhook) or use `--retry-while-unknown` |
| Verification failed | Provider breaks the contract | Fix provider, or negotiate a consumer change |
| No version in environment | `record-deployment` was never called for the counterpart | Add `record-deployment` to the counterpart's pipeline and backfill the current prod version |
| Unknown pacticipant/version | Wrong name or version string | Use the exact pacticipant name and the commit SHA |
## Message (async) contracts
For event payloads, consumers write message pacts describing the event they can process;
the provider verification produces the message from real code (a function returning the
payload) instead of an HTTP call. Use the same broker, selectors, and deploy gate.
## Rolling out to an existing system
1. Start with one high-traffic consumer/provider pair and one interaction.
2. Run provider verification as non-blocking for a week; fix noise (exact matchers,
missing states).
3. Make `can-i-deploy` blocking for that pair, then expand pair by pair.
4. Backfill `record-deployment` for versions already in each environment before turning
on the gate, or the first deploy will be blocked.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.