Pact
Pact: consumer-driven contract testing, provider verification, pact broker, API compatibility, microservice integration testing
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 linesPact — 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
givenclause) and implement correspondingstateHandlerson 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-deployhas 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
Related Skills
Cypress
"Cypress: end-to-end/component testing, commands, intercept (network mocking), fixtures, custom commands, CI, retries"
K6
k6: load testing, performance testing, stress testing, soak testing, thresholds, checks, scenarios, browser testing, CI integration
MSW
"MSW (Mock Service Worker): API mocking, request handlers, rest/graphql, browser/node, setupServer, network-level interception"
Playwright
"Playwright: end-to-end testing, browser automation, selectors, assertions, fixtures, page objects, visual regression, CI, codegen"
Storybook Test
Storybook Test: component testing via play functions, interaction testing, accessibility checks, visual regression, test-runner, portable stories
Testing Library
"Testing Library: React/DOM testing, queries (getBy/findBy/queryBy), user events, screen, waitFor, render, accessibility-first"