CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/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.

80

Quality

100%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files
name:
pact-contract-testing
description:
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.

pact-contract-testing

Overview

Pact is a code-first tool for consumer-driven contract tests over HTTP and message integrations (pact-overview). 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):

  1. Consumer test runs against a Pact mock server using the Pact DSL.
  2. The framework writes a pact file (JSON) capturing every given() → uponReceiving() → withRequest() → willRespondWith() interaction.
  3. The pact file is published to the Pact Broker.
  4. The provider verifies the pact: each recorded request is replayed against the running provider and responses are checked against the contract.
  5. can-i-deploy queries the Broker matrix to confirm the candidate version has a green verification against every consumer/provider already deployed in the target environment (can-i-deploy).

When to use

  • Two or more services communicate over HTTP/JSON or a message bus.
  • The team owns both consumer and provider - Pact requires provider buy-in for verification.
  • You want a safety net richer than schema comparison: contract-by-example asserts what consumers actually use, not the full provider surface.
  • The repo already imports @pact-foundation/pact, pact-jvm-consumer, pact-python, etc., or a .pact/ directory is present.

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.)

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.)

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). 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_TOKEN

The Broker stores (overview):

  • The pact files themselves.
  • Provider verification results per consumer version.
  • Tags and branches that map app versions to lifecycle environments (e.g. production, staging, main).

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.)

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). 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:

pact-broker can-i-deploy \
  --pacticipant pet-service \
  --version $(git rev-parse HEAD) \
  --to-environment production
FlagRequiredEffect
--pacticipantyesApplication (consumer or provider) name.
--versionyesApp version string (typically the Git SHA).
--to-environmentoptionalTarget environment to check against. Omit to check against latest.

Exit codes (can-i-deploy):

  • 0 - "Computer says yes \o/" - safe to deploy.
  • 1 - "Computer says no" - at least one matrix cell is missing or failed.

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 production

Skipping 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

Workspace
testland
Visibility
Public
Created
Last updated
Publish Source
GitHub
Badge
testland/pact-contract-testing badge