Skip to main content
Technology & EngineeringApi Testing228 lines

Contract Testing

Pact contract testing for consumer-driven API contracts between microservices

Quick Summary31 lines
You are an expert in Pact for consumer-driven contract testing between API consumers and providers.

## Key Points

- Write consumer tests first — they define what the consumer actually needs, not what the provider happens to return.
- Use matchers (`like`, `eachLike`) instead of exact values so contracts verify shape and type, not specific data.
- Keep provider state handlers (`stateHandlers`) simple — seed minimal data needed to satisfy each `given` state.
- Run `can-i-deploy` in your CI pipeline before every deployment to prevent breaking changes.
- Tag pacts with branch name and environment so the broker can track which versions are compatible.
- Each consumer should have its own contract — do not combine multiple consumers into one pact.
- Testing too much in contracts — contract tests verify the interface, not business logic. Do not assert on exact field counts or specific business rules.
- Forgetting to implement `stateHandlers` on the provider side, causing verification to fail because the required data does not exist.
- Not publishing verification results back to the broker, which means `can-i-deploy` cannot function correctly.
- Using exact value matching instead of type matchers, making contracts brittle and prone to false failures.
- Treating contract tests as a replacement for integration tests — they complement each other but serve different purposes.

## Quick Example

```bash
npm install --save-dev @pact-foundation/pact
```

```bash
# Publish from CLI
npx pact-broker publish ./pact/pacts \
  --consumer-app-version=$(git rev-parse --short HEAD) \
  --broker-base-url=http://localhost:9292 \
  --tag=$(git branch --show-current)
```
skilldb get api-testing-skills/Contract TestingFull skill: 228 lines
Paste into your CLAUDE.md or agent config

Contract Testing (Pact) — API Testing

You are an expert in Pact for consumer-driven contract testing between API consumers and providers.

Core Philosophy

Overview

Pact is a contract testing framework that verifies interactions between API consumers and providers without requiring integration environments. The consumer defines expected interactions (contracts), which are published to a Pact Broker. The provider then independently verifies it can fulfill those contracts. This catches integration bugs at build time rather than in staging.

Setup & Configuration

Installation (JavaScript/TypeScript)

npm install --save-dev @pact-foundation/pact

Project structure

consumer-service/
  tests/
    pact/
      user-api.consumer.pact.test.ts
  pact/
    pacts/          # Generated pact files land here

provider-service/
  tests/
    pact/
      user-api.provider.pact.test.ts

Pact Broker (Docker)

# docker-compose.yml
services:
  pact-broker:
    image: pactfoundation/pact-broker:latest
    ports:
      - "9292:9292"
    environment:
      PACT_BROKER_DATABASE_URL: sqlite:///pact_broker.sqlite

Or use PactFlow as a managed broker.

Core Patterns

Consumer test

The consumer defines what it expects from the provider:

import { PactV3, MatchersV3 } from "@pact-foundation/pact";
import { UserApiClient } from "../src/user-api-client";

const { like, eachLike, string, integer } = MatchersV3;

const provider = new PactV3({
  consumer: "frontend-app",
  provider: "user-service",
  dir: "./pact/pacts",
});

describe("User API - Consumer", () => {
  it("returns a user by ID", async () => {
    await provider
      .given("a user with ID 42 exists")
      .uponReceiving("a request for user 42")
      .withRequest({
        method: "GET",
        path: "/api/users/42",
        headers: { Accept: "application/json" },
      })
      .willRespondWith({
        status: 200,
        headers: { "Content-Type": "application/json" },
        body: like({
          id: integer(42),
          name: string("Alice"),
          email: string("alice@example.com"),
        }),
      })
      .executeTest(async (mockServer) => {
        const client = new UserApiClient(mockServer.url);
        const user = await client.getUser(42);

        expect(user.id).toBe(42);
        expect(user.name).toBeDefined();
        expect(user.email).toBeDefined();
      });
  });

  it("returns 404 when user not found", async () => {
    await provider
      .given("no user with ID 999 exists")
      .uponReceiving("a request for a non-existent user")
      .withRequest({
        method: "GET",
        path: "/api/users/999",
        headers: { Accept: "application/json" },
      })
      .willRespondWith({
        status: 404,
        body: like({ error: string("User not found") }),
      })
      .executeTest(async (mockServer) => {
        const client = new UserApiClient(mockServer.url);
        await expect(client.getUser(999)).rejects.toThrow("User not found");
      });
  });
});

Provider verification test

The provider verifies it can fulfill the consumer's contract:

import { Verifier } from "@pact-foundation/pact";
import app from "../src/app";

describe("User API - Provider verification", () => {
  let server: any;

  beforeAll(async () => {
    server = app.listen(4000);
  });

  afterAll(() => server.close());

  it("validates the contract against the consumer", async () => {
    const verifier = new Verifier({
      providerBaseUrl: "http://localhost:4000",
      pactBrokerUrl: "http://localhost:9292",
      provider: "user-service",
      publishVerificationResult: true,
      providerVersion: process.env.GIT_SHA || "local",
      stateHandlers: {
        "a user with ID 42 exists": async () => {
          // Seed the database with user 42
          await db.users.create({ id: 42, name: "Alice", email: "alice@example.com" });
        },
        "no user with ID 999 exists": async () => {
          // Ensure user 999 does not exist
          await db.users.deleteWhere({ id: 999 });
        },
      },
    });

    await verifier.verifyProvider();
  });
});

Publishing pacts to the broker

# Publish from CLI
npx pact-broker publish ./pact/pacts \
  --consumer-app-version=$(git rev-parse --short HEAD) \
  --broker-base-url=http://localhost:9292 \
  --tag=$(git branch --show-current)

Can-I-Deploy check

Before deploying, verify all contracts are satisfied:

npx pact-broker can-i-deploy \
  --pacticipant=user-service \
  --version=$(git rev-parse --short HEAD) \
  --to-environment=production \
  --broker-base-url=http://localhost:9292

Matchers

import { MatchersV3 } from "@pact-foundation/pact";

// Flexible matchers for contract verification
like({ name: "Alice" })               // Matches type, not exact value
eachLike({ id: 1, name: "Alice" })    // Array where each element matches shape
string("hello")                         // Any string
integer(42)                             // Any integer
decimal(3.14)                           // Any decimal
boolean(true)                           // Any boolean
regex("2024-01-01", /^\d{4}-\d{2}-\d{2}$/)  // Matches regex pattern

Best Practices

  • Write consumer tests first — they define what the consumer actually needs, not what the provider happens to return.
  • Use matchers (like, eachLike) instead of exact values so contracts verify shape and type, not specific data.
  • Keep provider state handlers (stateHandlers) simple — seed minimal data needed to satisfy each given state.
  • Run can-i-deploy in your CI pipeline before every deployment to prevent breaking changes.
  • Tag pacts with branch name and environment so the broker can track which versions are compatible.
  • Each consumer should have its own contract — do not combine multiple consumers into one pact.

Common Pitfalls

  • Testing too much in contracts — contract tests verify the interface, not business logic. Do not assert on exact field counts or specific business rules.
  • Forgetting to implement stateHandlers on the provider side, causing verification to fail because the required data does not exist.
  • Not publishing verification results back to the broker, which means can-i-deploy cannot function correctly.
  • Using exact value matching instead of type matchers, making contracts brittle and prone to false failures.
  • Treating contract tests as a replacement for integration tests — they complement each other but serve different purposes.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add api-testing-skills

Get CLI access →