pact-contract-testing
Authors and verifies Pact consumer-driven contract tests across the full Pact lifecycle - consumer tests producing pact files, publishing to the Pact Broker, provider verification, and `can-i-deploy` deployment gates. Use when introducing a new HTTP/JSON API contract between two services, diagnosing breaking changes, or wiring contract verification into CI.
Install with skills.sh (any agent)
npx skills add testland/qa --skill pact-contract-testingpact-contract-testing
Overview
Pact is a code-first tool for consumer-driven contract tests over HTTP and message integrations (pact-overview (opens in new window)). The contract is a side-effect of the consumer's tests - each test documents one request/response pair, and only the parts the consumer actually uses get tested.
The lifecycle has five steps (pact-how-it-works (opens in new window)):
When to use
If the API has no consumers under your control (public or third-party APIs), prefer openapi-contract-diff - schema diffs need no consumer coordination.
Authoring (consumer side)
Install (Node example)
npm install --save-dev @pact-foundation/pact(Per pact-js (opens in new window).)
Consumer test with PactV3
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like, eachLike } = MatchersV3;
const provider = new PactV3({
consumer: 'web-app',
provider: 'pet-service',
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('Pet Service consumer', () => {
it('returns a list of dogs', async () => {
provider
.given('I have a list of dogs')
.uponReceiving('a request for all dogs with the builder pattern')
.withRequest({
method: 'GET',
path: '/dogs',
headers: { Accept: 'application/json' },
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: eachLike({
id: like(1),
name: like('Rex'),
}),
});
await provider.executeTest(async (mockServer) => {
const response = await fetch(`${mockServer.url}/dogs`);
expect(response.status).toBe(200);
});
});
});(Adapted from pact-js (opens in new window).)
given() describes a provider state the verifier sets up later. Matchers like like() and eachLike() assert type/shape rather than exact values, so the provider isn't bound to fixture-specific data.
Where pact files are written
By default, PactV3 writes pact files to ./pacts/ relative to the process CWD (pact-js (opens in new window)). Each consumer/provider pair produces one JSON file: <consumer>-<provider>.json.
Publishing to the Pact Broker
The Broker is the central registry for pact files and verification results. Publishing happens after the consumer tests pass:
npx pact-broker publish ./pacts \
--consumer-app-version=$(git rev-parse HEAD) \
--branch=$(git rev-parse --abbrev-ref HEAD) \
--broker-base-url=$PACT_BROKER_BASE_URL \
--broker-token=$PACT_BROKER_TOKENThe Broker stores (overview (opens in new window)):
Tagging by branch + the Git SHA as consumer-app-version is the canonical pattern - can-i-deploy queries the matrix using these identifiers.
Provider verification
const { Verifier } = require('@pact-foundation/pact');
new Verifier({
providerBaseUrl: 'http://localhost:8081',
pactBrokerUrl: process.env.PACT_BROKER_BASE_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
provider: 'pet-service',
providerVersion: process.env.GIT_SHA,
providerVersionBranch: process.env.GIT_BRANCH,
publishVerificationResult: true,
consumerVersionSelectors: [
{ mainBranch: true },
{ deployedOrReleased: true },
],
}).verifyProvider();(Adapted from pact-js (opens in new window).)
consumerVersionSelectors controls which consumer pacts get verified - mainBranch plus deployedOrReleased keeps the provider compatible with both the latest consumer work and what's in production (pact-how-it-works (opens in new window)). publishVerificationResult: true is what updates the matrix - without it the broker has no record of this provider version's verification status.
Provider states
For each consumer interaction with a given(<state>), the provider test setup must register a hook that puts the system into that state before replay. Using Express:
new Verifier({
...
stateHandlers: {
'I have a list of dogs': async () => {
await db.dogs.bulkInsert([{ id: 1, name: 'Rex' }]);
return { description: 'dogs seeded' };
},
},
}).verifyProvider();State setup that fails causes the matching interaction to fail verification.
can-i-deploy - deployment gate
Per can-i-deploy (opens in new window):
pact-broker can-i-deploy \
--pacticipant pet-service \
--version $(git rev-parse HEAD) \
--to-environment production| Flag | Required | Effect |
|---|---|---|
--pacticipant | yes | Application (consumer or provider) name. |
--version | yes | App version string (typically the Git SHA). |
--to-environment | optional | Target environment to check against. Omit to check against latest. |
Exit codes (can-i-deploy (opens in new window)):
The output includes a markdown-style matrix of consumer/provider version pairs with verification status and links to detailed verification results.
Recording deployments
After deploying, record the deployment so subsequent can-i-deploy queries see the new "currently deployed" baseline:
pact-broker record-deployment \
--pacticipant pet-service \
--version $(git rev-parse HEAD) \
--environment productionSkipping this step is the most common reason can-i-deploy returns spurious "no" verdicts.
CI integration
Wire publish -> verify -> can-i-deploy into the pipeline for both sides. Full consumer and provider GitHub Actions steps are in references/ci-pipelines.md.
can-i-deploy is the actual gate - pact verification happens in step 1, but a green verification alone doesn't prove every paired version is compatible.
References
Pact CI integration
View source (opens in new window)Pact CI integration
A complete CI flow per side wires publish, provider verification, and the can-i-deploy gate into the pipeline. Both sides read PACT_BROKER_BASE_URL and PACT_BROKER_TOKEN from CI secrets.
Consumer pipeline
- name: Run consumer tests + publish pact
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
run: |
npm test # writes ./pacts/<consumer>-<provider>.json
npx pact-broker publish ./pacts \
--consumer-app-version=$GITHUB_SHA \
--branch=${GITHUB_REF##*/}
- name: Can I deploy?
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
run: |
pact-broker can-i-deploy \
--pacticipant=web-app \
--version=$GITHUB_SHA \
--to-environment=productionProvider pipeline
- name: Verify pacts from broker
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
run: |
npm run start:provider &
npx wait-on http://localhost:8081
npm run verify:pact # publishVerificationResult: true
- name: Can I deploy?
env:
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_BASE_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
run: |
pact-broker can-i-deploy \
--pacticipant=pet-service \
--version=$GITHUB_SHA \
--to-environment=productioncan-i-deploy is the actual gate - pact verification happens in step 1, but a green verification alone doesn't prove every paired version is compatible.
Related skills
contract-compatibility-gate
Builds a unified deployment-readiness gate that aggregates verdicts from any combination of Pact `can-i-deploy`, oasdiff (OpenAPI), graphql-inspector (GraphQL), and `buf breaking` (Protobuf), applies severity-aware pass/fail thresholds, and emits a single go / no-go decision with per-finding rationale. Use when authoring a CI step that gates a deployment on cross-protocol contract compatibility.
graphql-schema-regression
Diffs two GraphQL schemas with `graphql-inspector diff`, classifies each change as BREAKING / DANGEROUS / NON_BREAKING, and applies opt-in rules (suppress-removal-of-deprecated-field, consider-usage, ignore-description-changes) to gate CI on schema regressions. Use when reviewing GraphQL schema changes in a PR or evaluating Federation supergraph drift.
openapi-contract-diff
Diffs two OpenAPI versions for breaking changes (removed endpoints, deleted schemas, narrowed enums, response shape changes) using `oasdiff breaking`, severity-classifies the findings (ERR / WARN / INFO), and gates CI with `--fail-on`. Use when reviewing an OpenAPI spec change in a PR or on a release tag.
protobuf-compat-checking
Wraps `buf breaking` to compare a Protobuf schema against a past version, classifies breaking changes by category (FILE / PACKAGE / WIRE_JSON / WIRE), configures buf.yaml rule selection plus per-path exemptions, and gates CI on the exit code. Use when reviewing `.proto` changes in a PR for gRPC services, BSR modules, or schema-versioned message bus payloads.