Skip to main content
Technology & EngineeringTesting Services208 lines

Pact

Pact: consumer-driven contract testing, provider verification, pact broker, API compatibility, microservice integration testing

Quick Summary20 lines
You are an expert in using Pact for consumer-driven contract testing between services.

## Key Points

- Keep contract tests focused on the shape and status codes of interactions, not business logic; test business rules in unit tests on each side independently.
- Use a Pact Broker (or PactFlow) to share contracts between repositories and enable "can-i-deploy" checks in CI pipelines before releasing a service.
- Define meaningful provider states (the `given` clause) and implement corresponding `stateHandlers` on the provider side so verification runs against realistic data scenarios.
- Forgetting to publish verification results back to the broker; without this, `can-i-deploy` has no data and cannot gate deployments, defeating the purpose of contract testing in a CI/CD pipeline.

## Quick Example

```bash
# Consumer side (JavaScript/TypeScript)
npm install -D @pact-foundation/pact

# For sharing pacts, install the Pact Broker CLI or use PactFlow (SaaS)
# docker run -d -p 9292:9292 pactfoundation/pact-broker
```
skilldb get testing-services-skills/PactFull skill: 208 lines
Paste into your CLAUDE.md or agent config

Pact — Testing

You are an expert in using Pact for consumer-driven contract testing between services.

Core Philosophy

Overview

Pact is a contract testing framework that verifies the integration between services without requiring them to run simultaneously. The consumer (client) defines the interactions it expects from a provider (server) and generates a contract file (a "pact"). The provider then replays those interactions against its real implementation to confirm it satisfies the contract. This catches breaking API changes early, runs fast because each side tests independently, and replaces brittle end-to-end integration tests between microservices. Pact supports HTTP REST and message-based (event/queue) interactions.

Setup & Configuration

Installation

# Consumer side (JavaScript/TypeScript)
npm install -D @pact-foundation/pact

# For sharing pacts, install the Pact Broker CLI or use PactFlow (SaaS)
# docker run -d -p 9292:9292 pactfoundation/pact-broker

Consumer test setup

// consumer/pact.setup.ts
import { PactV4 } from "@pact-foundation/pact";
import path from "path";

export const provider = new PactV4({
  consumer: "frontend-app",
  provider: "user-service",
  dir: path.resolve(process.cwd(), "pacts"), // where contract files are written
  logLevel: "warn",
});

Provider verification setup

// provider/pact.verify.ts
import { Verifier } from "@pact-foundation/pact";

const verifier = new Verifier({
  providerBaseUrl: "http://localhost:3001",
  pactUrls: [
    // Local file or broker URL
    "./pacts/frontend-app-user-service.json",
  ],
  // Or use a broker:
  // pactBrokerUrl: "https://your-broker.pactflow.io",
  // pactBrokerToken: process.env.PACT_BROKER_TOKEN,
  // publishVerificationResult: process.env.CI === "true",
  // providerVersion: process.env.GIT_SHA,
  stateHandlers: {
    "a user with ID 1 exists": async () => {
      // Seed test data for this provider state
      await seedUser({ id: "1", name: "Alice", email: "alice@test.com" });
    },
  },
});

Core Patterns

Writing a consumer contract test

// consumer/user-api.pact.test.ts
import { provider } from "./pact.setup";
import { UserApiClient } from "../src/api/user-client";

describe("User API contract", () => {
  it("fetches a user by ID", async () => {
    await provider
      .addInteraction()
      .given("a user with ID 1 exists")
      .uponReceiving("a request to get user 1")
      .withRequest("GET", "/api/users/1", (builder) => {
        builder.headers({ Accept: "application/json" });
      })
      .willRespondWith(200, (builder) => {
        builder
          .headers({ "Content-Type": "application/json" })
          .jsonBody({
            id: "1",
            name: "Alice",
            email: "alice@test.com",
          });
      })
      .executeTest(async (mockServer) => {
        const client = new UserApiClient(mockServer.url);
        const user = await client.getUser("1");

        expect(user.id).toBe("1");
        expect(user.name).toBe("Alice");
        expect(user.email).toBe("alice@test.com");
      });
  });

  it("returns 404 for a missing user", async () => {
    await provider
      .addInteraction()
      .given("no user with ID 99 exists")
      .uponReceiving("a request for a non-existent user")
      .withRequest("GET", "/api/users/99")
      .willRespondWith(404, (builder) => {
        builder.jsonBody({ error: "User not found" });
      })
      .executeTest(async (mockServer) => {
        const client = new UserApiClient(mockServer.url);
        await expect(client.getUser("99")).rejects.toThrow("User not found");
      });
  });
});

Provider verification in CI

// provider/pact.verify.test.ts
import { Verifier } from "@pact-foundation/pact";

describe("Provider verification", () => {
  // Start your real provider server before this runs

  it("satisfies all consumer contracts", async () => {
    const output = await new Verifier({
      providerBaseUrl: "http://localhost:3001",
      pactBrokerUrl: process.env.PACT_BROKER_URL,
      pactBrokerToken: process.env.PACT_BROKER_TOKEN,
      provider: "user-service",
      providerVersion: process.env.GIT_SHA,
      publishVerificationResult: process.env.CI === "true",
      enablePending: true, // allow new, unverified pacts without breaking the build
      stateHandlers: {
        "a user with ID 1 exists": async () => {
          await db.users.create({ id: "1", name: "Alice", email: "alice@test.com" });
        },
        "no user with ID 99 exists": async () => {
          await db.users.deleteAll();
        },
      },
    }).verifyProvider();

    console.log("Verification complete:", output);
  });
});

Message-based contract (event/queue)

// consumer/order-events.pact.test.ts
import { PactV4 } from "@pact-foundation/pact";

const messagePact = new PactV4({
  consumer: "order-processor",
  provider: "order-service",
  dir: "./pacts",
});

describe("Order events contract", () => {
  it("processes an OrderCreated event", async () => {
    await messagePact
      .addInteraction()
      .given("an order has been placed")
      .expectsToReceive("an OrderCreated event")
      .withMetadata({ topic: "orders" })
      .withContent({ orderId: "abc-123", total: 49.99, currency: "USD" })
      .toMessagePromise()
      .then((message) => {
        const event = JSON.parse(message.contents.toString());
        expect(event.orderId).toBe("abc-123");
        expect(event.total).toBe(49.99);
      });
  });
});

Best Practices

  • Keep contract tests focused on the shape and status codes of interactions, not business logic; test business rules in unit tests on each side independently.
  • Use a Pact Broker (or PactFlow) to share contracts between repositories and enable "can-i-deploy" checks in CI pipelines before releasing a service.
  • Define meaningful provider states (the given clause) and implement corresponding stateHandlers on the provider side so verification runs against realistic data scenarios.

Common Pitfalls

  • Testing too many scenarios in contracts instead of covering only the interactions the consumer actually uses; over-specifying couples the consumer to provider implementation details and makes contracts fragile.
  • Forgetting to publish verification results back to the broker; without this, can-i-deploy has no data and cannot gate deployments, defeating the purpose of contract testing in a CI/CD pipeline.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

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

Get CLI access →