Pact Cheat Sheet
Overview
Pact is a code-first consumer-driven contract testing framework that ensures service compatibility without requiring integration tests against live services. The consumer defines the expected interactions (the contract or “pact”), and the provider verifies that it can fulfill those expectations. This enables teams to independently deploy microservices with confidence.
Pact supports HTTP API testing and message-based interactions across multiple languages including JavaScript, Java, Python, Go, Ruby, .NET, and Rust. The Pact Broker (or PactFlow SaaS) stores and manages contracts, tracks verification results, and provides a “can-i-deploy” check for CI/CD pipelines to prevent incompatible deployments.
Installation
# JavaScript / Node.js
npm install -D @pact-foundation/pact
# Python
pip install pact-python
# Go
go get github.com/pact-foundation/pact-go/v2
# Ruby
gem install pact
# .NET
dotnet add package PactNet
# Pact CLI tools
npm install -g @pact-foundation/pact-cli
# Pact Broker (Docker)
docker pull pactfoundation/pact-broker
Consumer Side (JavaScript)
const { PactV3 } = require("@pact-foundation/pact");
const { describe, it } = require("mocha");
const { expect } = require("chai");
const axios = require("axios");
const provider = new PactV3({
consumer: "OrderService",
provider: "UserService",
dir: "./pacts",
});
describe("User API", () => {
it("should return user by ID", async () => {
// Arrange: define the expected interaction
provider
.given("a user with ID 123 exists")
.uponReceiving("a request for user 123")
.withRequest({
method: "GET",
path: "/api/users/123",
headers: { Accept: "application/json" },
})
.willRespondWith({
status: 200,
headers: { "Content-Type": "application/json" },
body: {
id: 123,
name: "John Doe",
email: "john@example.com",
},
});
// Act & Assert: run the test against the mock
await provider.executeTest(async (mockserver) => {
const response = await axios.get(`${mockserver.url}/api/users/123`, {
headers: { Accept: "application/json" },
});
expect(response.data.name).to.equal("John Doe");
expect(response.data.email).to.equal("john@example.com");
});
});
});
Matchers
const { MatchersV3 } = require("@pact-foundation/pact");
const {
like,
eachLike,
regex,
integer,
decimal,
boolean,
string,
timestamp,
uuid,
} = MatchersV3;
// Flexible matching
provider
.uponReceiving("a list of users")
.withRequest({ method: "GET", path: "/api/users" })
.willRespondWith({
status: 200,
body: {
users: eachLike({
id: integer(1),
name: string("John Doe"),
email: regex("john@example.com", "^[\\w.]+@[\\w.]+$"),
active: boolean(true),
balance: decimal(99.99),
createdAt: timestamp("2024-01-15T10:30:00Z", "yyyy-MM-dd'T'HH:mm:ss'Z'"),
uuid: uuid("12345678-1234-1234-1234-123456789012"),
}),
total: integer(100),
},
});
Provider Side Verification
const { Verifier } = require("@pact-foundation/pact");
describe("Provider Verification", () => {
it("should validate the expectations of OrderService", async () => {
const opts = {
provider: "UserService",
providerBaseUrl: "http://localhost:3000",
// From Pact Broker
pactBrokerUrl: "https://your-broker.pactflow.io",
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
publishVerificationResult: true,
providerVersion: process.env.GIT_COMMIT,
providerVersionBranch: process.env.GIT_BRANCH,
// Or from local files
// pactUrls: ['./pacts/orderservice-userservice.json'],
// State handlers
stateHandlers: {
"a user with ID 123 exists": async () => {
await seedDatabase({ id: 123, name: "John Doe" });
},
"no users exist": async () => {
await clearDatabase();
},
},
};
await new Verifier(opts).verifyProvider();
});
});
Pact Broker
# Run Pact Broker with Docker Compose
# docker-compose.yml
# services:
# pact-broker:
# image: pactfoundation/pact-broker
# ports:
# - "9292:9292"
# environment:
# PACT_BROKER_DATABASE_URL: "sqlite:///pact_broker.sqlite3"
# Publish pacts to broker
npx pact-broker publish ./pacts \
--broker-base-url https://your-broker.pactflow.io \
--broker-token $PACT_BROKER_TOKEN \
--consumer-app-version $(git rev-parse HEAD) \
--branch $(git branch --show-current) \
--tag-with-git-branch
# Can I deploy check
npx pact-broker can-i-deploy \
--pacticipant OrderService \
--version $(git rev-parse HEAD) \
--to-environment production \
--broker-base-url https://your-broker.pactflow.io \
--broker-token $PACT_BROKER_TOKEN
# Record deployment
npx pact-broker record-deployment \
--pacticipant OrderService \
--version $(git rev-parse HEAD) \
--environment production
Pact CLI Commands
| Command | Description |
|---|---|
pact-broker publish | Publish pact files to the broker |
pact-broker can-i-deploy | Check if a version is safe to deploy |
pact-broker record-deployment | Record a deployment to an environment |
pact-broker record-release | Record a release to an environment |
pact-broker create-environment | Create a new environment |
pact-broker list-latest-pact-versions | List latest pact versions |
pact-broker create-or-update-webhook | Configure webhooks |
Message-Based Pacts
// Consumer side - message handler
const { MessageConsumerPact } = require("@pact-foundation/pact");
const messagePact = new MessageConsumerPact({
consumer: "NotificationService",
provider: "OrderService",
dir: "./pacts",
});
describe("Order events", () => {
it("should handle order created event", () => {
return messagePact
.given("an order is created")
.expectsToReceive("an order created event")
.withContent({
orderId: like("order-123"),
userId: like("user-456"),
total: like(99.99),
items: eachLike({ productId: like("prod-1"), quantity: integer(1) }),
})
.verify(async (message) => {
const handler = new OrderEventHandler();
await handler.handle(JSON.parse(message));
// Assert side effects
});
});
});
Advanced Usage
Provider States with Parameters
// Consumer
provider
.given("a user exists", { userId: "123", name: "John" })
.uponReceiving("a request for user details")
.withRequest({ method: "GET", path: "/api/users/123" })
.willRespondWith({ status: 200 });
// Provider state handler
stateHandlers: {
"a user exists": async (params) => {
await createUser({ id: params.userId, name: params.name });
},
}
Pending Pacts and WIP
const opts = {
provider: "UserService",
providerBaseUrl: "http://localhost:3000",
pactBrokerUrl: "https://your-broker.pactflow.io",
enablePending: true,
includeWipPactsSince: "2024-01-01",
consumerVersionSelectors: [
{ mainBranch: true },
{ deployedOrReleased: true },
],
};
Webhook Configuration
# Trigger provider verification when pact changes
npx pact-broker create-or-update-webhook \
https://ci.example.com/trigger-verify \
--broker-base-url https://your-broker.pactflow.io \
--description "Trigger UserService verification" \
--contract-content-changed \
--provider UserService
Configuration
# Environment variables
export PACT_BROKER_BASE_URL="https://your-broker.pactflow.io"
export PACT_BROKER_TOKEN="your-read-write-token"
export PACT_DO_NOT_TRACK=true
# .pactrc (Ruby)
# --pact-broker-base-url https://your-broker.pactflow.io
# --broker-token your-token
Troubleshooting
| Issue | Solution |
|---|---|
| Pact verification fails | Compare consumer expectations with actual provider responses |
| State handler not found | Ensure provider state name matches exactly between consumer and provider |
| Broker publish fails | Check authentication token and broker URL |
can-i-deploy returns false | Check which consumer/provider pair has unverified pacts |
| Matcher mismatch | Use like() for structure matching, regex() for format matching |
| Message pact not generated | Ensure .verify() callback processes the message |
| Pending pacts ignored | Set enablePending: true in provider verification |
| Flaky provider tests | Ensure state handlers fully set up and tear down test data |