Contract Testing
Pact contract testing for consumer-driven API contracts between microservices
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 linesContract 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 eachgivenstate. - Run
can-i-deployin 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
stateHandlerson 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-deploycannot 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
Related Skills
API Mocking
API mocking with MSW (Mock Service Worker) and Prism for development and testing
Bruno
Bruno API client for git-friendly, offline-first API testing with Bru markup language
Httpie
HTTPie CLI for human-friendly API testing, scripting, and debugging from the terminal
Load Testing
k6 load testing for API performance, stress testing, and threshold-based CI checks
Postman
Postman collections, environments, pre-request scripts, tests, and Newman CLI automation
Supertest
Supertest for Node.js HTTP assertion testing with Express, Koa, and Fastify