| name | contract-testing |
| description | Implements consumer-driven contract testing so services deploy independently without a full integration environment — the consumer's unit tests record concrete request/response expectations against a stub (Pact `pact-jvm`/`pact-js`/`pact-python`, or Spring Cloud Contract DSL), the resulting contract (pact file / Spring stub jar) is published to a broker (Pact Broker / PactFlow) tagged by consumer version + branch + environment, the provider replays every expectation against its real app in CI with provider states (`@State` / `Given`) seeding data, and `pact-broker can-i-deploy --pacticipant X --version <git-sha> --to-environment production` gates the pipeline — plus webhook-triggered provider verification on contract change, bi-directional contracts (verify a provider's OpenAPI against consumer pacts without running the provider), pending/WIP pacts so a new consumer expectation never breaks the provider build, and version pinning via the consumer's git SHA with `record-deployment`/`record-release`. |
| when_to_use | You have ≥2 services that talk over HTTP/messages and want to catch integration breakage in fast unit-speed CI instead of a brittle shared E2E env — adding Pact or Spring Cloud Contract, wiring a Pact broker, gating deploys with can-i-deploy, or deciding consumer-driven vs bi-directional contracts. Distinct from rest-graphql-contract (defines the API spec/schema itself — OpenAPI/GraphQL SDL/JSON Schema; this skill tests that two specific deployed versions actually agree) and schema-evolution-compatibility (the back/forward-compat rules a change must obey; this skill is the CI mechanism that proves a given consumer↔provider pair still satisfies them). |
When to Use
Reach for this skill when two or more independently deployed services integrate and you want integration confidence at unit-test speed, not via a fragile end-to-end stack:
- "Provider changed a field and a consumer broke in prod — catch it in CI before merge"
- "Our shared staging/E2E env is flaky and slow; we want to test integration without it"
- "Add Pact / Spring Cloud Contract between our frontend/BFF and the API"
- "Gate the deploy: don't ship the provider until every consumer's contract still passes"
- "We already have an OpenAPI spec — verify the provider matches it AND the consumers (bi-directional)"
- "A new consumer's expectation shouldn't be able to red the provider's build (pending pacts)"
- "Mobile app v3 is still live; how do we know the provider didn't drop a field v3 needs?"
NOT this skill:
- Authoring the API spec/schema (OpenAPI, GraphQL SDL, JSON Schema, field types, pagination shape) → rest-graphql-contract (defines what the API is; this skill proves two running versions agree on it)
- The back/forward-compatibility rules (additive-only, never-remove-required, default-on-new-optional) → schema-evolution-compatibility (the policy; this skill is the per-pair CI enforcement of it)
- gRPC/protobuf service definition and codegen → design-protobuf-grpc-service (you can still Pact-test gRPC via message pacts, but the
.proto itself lives there)
- General API design / breaking-change review of a diff → api-design-review
- Browser/UI end-to-end flows across the whole app → write-playwright-e2e (this skill replaces most cross-service E2E with isolated pair contracts)
- Structuring the unit-test suite itself / assertions / fixtures → write-tests, test-data-factories (this skill specifies the contract interactions; those build the surrounding suite/data)
- Wiring the CI stages / runners / caching → cicd-pipeline-author; the deploy gate's release flow → deploy-release (this skill supplies the can-i-deploy check those stages run)
Steps
-
Pick consumer-driven (Pact) when consumers know what they need; bi-directional/spec-driven when the provider already owns an OpenAPI/GraphQL spec. They are not interchangeable:
| Approach | How it works | Use when | Limitation |
|---|
| Consumer-driven (Pact) | consumer's tests generate expectations; provider replays them against the real app | consumers drive the API; you want to know exactly which fields are used | provider must run verification against real code; needs provider states |
| Bi-directional (PactFlow) | provider's OpenAPI is verified as a "provider contract"; consumer pacts compared statically against it — provider need not run | provider already has a trustworthy spec; can't run full provider verification | only as good as the spec; a spec that lies passes |
| Spring Cloud Contract | contracts in Groovy/YAML DSL live with the provider; generate provider tests + a stub jar consumers run against | JVM-heavy estate, provider-owned contracts, message + HTTP | JVM-centric; less natural for polyglot consumers |
Default to consumer-driven Pact for polyglot HTTP/message estates; Spring Cloud Contract for an all-JVM shop; add bi-directional when a provider can't feasibly run verification but has a real OpenAPI.
-
Write the consumer test against a Pact mock — assert on the request you send and matchers (not literals) for the response. The consumer test spins up Pact's local mock server, you exercise your real client code against it, and Pact records the interaction. Use matchers so the contract pins structure/type, not brittle example values:
const { PactV3, MatchersV3: M } = require('@pact-foundation/pact');
const provider = new PactV3({ consumer: 'web-bff', provider: });
provider
.()
.()
.({ : , : ,
: { : } })
.({ : ,
: { : M.(, ) },
: { : M.(), : M.(),
: M.(, ),
: M.({ : M.(), : M.() }) } });
provider.( (mock.).());
Common Errors
- Asserting on literal example values instead of matchers. Hardcoding
total: 19.99 means any data change reds provider verification. Fix: M.decimal()/integer()/regex()/eachLike() — pin type/structure, not the example.
- Consumer over-specifies fields it doesn't use. Asserting on every response field couples you to the provider's full shape and blocks its additive changes. Fix: assert only the fields the consumer reads; extra provider fields must pass.
- Provider state string ≠
@State/Given handler. given('order exists') vs @State("order 42 exists") → "no state handler" verification failure. Fix: keep the strings byte-identical; treat them as a shared contract.
- Verifying the provider against mocks/in-memory stubs. Defeats the purpose — you prove the mock matches, not the real app. Fix: run verification against the real provider + test DB seeded by state handlers.
- Versioning pacts with
latest/timestamps instead of the git SHA. can-i-deploy's matrix needs unique, reproducible versions; "latest" makes the gate meaningless. Fix: --consumer-app-version <git-sha>, branch via --branch.
- Not publishing verification results (or publishing from local dev). Holes in the matrix → can-i-deploy can't answer → gate fails open or hangs. Fix: publish results only from CI, keyed to the provider SHA.
- Skipping can-i-deploy and just deploying. Contracts that aren't gated provide false safety. Fix: make can-i-deploy a required pipeline stage that fails the deploy on non-zero exit; add
record-deployment after.
- No pending pacts → new consumer expectation reds the provider main build. Teams get blocked on each other and disable Pact in frustration. Fix:
enablePending + WIP pacts; new expectations are non-blocking until first green.
- Treating Pact as full-coverage E2E. Pact verifies the request/response shape per interaction, not business correctness or multi-hop flows. Fix: keep a thin layer of true E2E for critical journeys; Pact replaces the broad, flaky middle.
- Forgetting multi-version providers (mobile).
record-deployment assumes one live version; old app builds still in the wild get dropped. Fix: record-release/record-support-ended so can-i-deploy keeps every supported app version in the matrix.
Verify
- Consumer test produces a pact at unit speed: running the consumer suite emits
pacts/<consumer>-<provider>.json with matchers (not literals), no provider or network involved.
- Provider verification replays real interactions: the provider's verification task pulls pacts from the broker, runs against the real app + seeded DB via every
@State/Given handler, and all interactions pass (or are explicitly pending).
- Matrix is complete both ways: the broker shows the consumer pact and a published verification result for the provider's version — no "unverified" holes.
- Gate actually blocks: introduce a breaking provider change (drop/rename a consumed field), run
can-i-deploy --to-environment production → it exits non-zero and the deploy stage fails; revert → exit 0.
- Additive change is safe: add a new optional field on the provider → consumer pact still verifies green and can-i-deploy passes (proves extra fields don't break consumers).
- Pending pacts don't red main: publish a new consumer expectation the provider doesn't yet satisfy → provider build reports it pending/non-blocking, not failed; once provider implements it and verifies, it becomes blocking.
- Versions are git SHAs: every publish/verify/record uses
git rev-parse versions; grep CI for latest/timestamp versions and remove them.
- Webhook closes the loop: publishing a changed contract auto-triggers the provider's verification pipeline; the broker reflects the fresh result without manual intervention.
- Multi-version handled (if applicable):
record-release keeps every supported mobile/app version in the matrix; can-i-deploy refuses a provider change that breaks any still-supported version.
Done = consumers generate matcher-based pacts at unit speed, the provider replays them against the real app with idempotent state handlers, verification results and deployments are recorded to the broker keyed by git SHA, every deploy is gated by can-i-deploy against the target environment, new expectations land as non-blocking pending pacts, and contract changes auto-trigger provider re-verification via webhook — proven by the breaking-change-blocks / additive-change-passes / pending-doesn't-red tests in checks 4–6.