Consumer-Driven Contract Testing with Pact
The Problem Pact Solves
In a microservices architecture, service A calls service B. If B changes its API, A breaks. But B's own tests pass because B does not know about A's expectations. Consumer-driven contracts flip this: the consumer (A) publishes a contract describing what it expects, and the provider (B) verifies it can fulfill that contract.
The Pact Workflow
+--------------+ +--------------+ +--------------+
| CONSUMER | | PACT BROKER | | PROVIDER |
| (Service A) | | (Registry) | | (Service B) |
| | | | | |
| 1. Write |--pact-->| 2. Store |--pact-->| 3. Verify |
| consumer | file | contract | file | provider |
| test | | | | test |
+--------------+ +--------------+ +--------------+
Step 1: Consumer Writes Tests
The consumer team writes tests that describe what they expect from the provider. These tests run against a Pact mock server, not the real provider.
Step 2: Pact Broker Stores Contracts
The generated Pact file (a JSON contract) is published to a central broker. The broker stores all consumer contracts and tracks which versions are compatible.
Step 3: Provider Verifies
The provider team runs verification tests that replay the consumer's expectations against the real provider implementation. If any expectation fails, the provider knows they would break a consumer.
Why Pact Matters for AI-Augmented Testing
Traditional Pact workflow requires manual contract writing -- a tedious process that teams often skip. AI changes this by:
- Analyzing client code to automatically identify API call patterns
- Generating consumer contracts from actual usage, not documentation
- Detecting contract drift when the provider changes
- Suggesting minimal compatible contracts when conflicts arise
Anatomy of a Pact Contract
{
"consumer": { "name": "StorefrontUI" },
"provider": { "name": "ProductService" },
"interactions": [
{
"description": "a request for product ABC-123",
"providerState": "product ABC-123 exists",
"request": {
"method": "GET",
"path": "/api/v2/products/ABC-123",
"headers": {
"Authorization": "Bearer valid-token"
}
},
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"body": {
"id": "ABC-123",
"name": "Blue Widget",
"price": 29.99,
"category": "electronics",
"in_stock": true
},
"matchingRules": {
"body": {
"$.id": { "matchers": [{ "match": "type" }] },
"$.name": { "matchers": [{ "match": "type" }] },
"$.price": { "matchers": [{ "match": "decimal" }] },
"$.category": { "matchers": [{ "match": "regex", "regex": "electronics|clothing|food|other" }] }
}
}
}
}
]
}
Key Concepts
Provider States: Preconditions that must be true for the interaction. "product ABC-123 exists" means the provider's verification test must set up this state before replaying the request.
Matching Rules: Instead of exact value matching, Pact uses flexible matchers:
type-- value must be the same type (string, number, boolean)regex-- value must match the patterndecimal-- value must be a decimal numberlike-- structure must match (same keys, compatible types)eachLike-- array where each element matches the example
This flexibility is critical: the consumer does not care that the product's name is exactly "Blue Widget" -- it only cares that a name field exists and is a string.
Writing Consumer Tests
JavaScript/TypeScript with Pact V4
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, uuid, decimal, term } = MatchersV3;
const provider = new PactV4({
consumer: 'StorefrontUI',
provider: 'ProductService',
});
describe('ProductClient Pact Tests', () => {
describe('getProduct', () => {
it('returns a product when it exists', async () => {
await provider
.addInteraction()
.given('product ABC-123 exists')
.uponReceiving('a request for product ABC-123')
.withRequest('GET', '/api/v2/products/ABC-123', (builder) => {
builder.headers({ Authorization: like('Bearer valid-token') });
})
.willRespondWith(200, (builder) => {
builder.jsonBody({
id: uuid(),
name: like('Blue Widget'),
price: decimal(29.99),
category: term({
generate: 'electronics',
regex: 'electronics|clothing|food|other'
}),
in_stock: like(true),
});
})
.executeTest(async (mockServer) => {
const client = new ProductClient(mockServer.url, 'valid-token');
const product = await client.getProduct('ABC-123');
expect(product.name).toBeDefined();
expect(product.price).toBeGreaterThanOrEqual(0);
});
});
it('returns 404 when product does not exist', async () => {
await provider
.addInteraction()
.given('product NONEXISTENT does not exist')
.uponReceiving('a request for a nonexistent product')
.withRequest('GET', '/api/v2/products/NONEXISTENT', (builder) => {
builder.headers({ Authorization: like('Bearer valid-token') });
})
.willRespondWith(404)
.executeTest(async (mockServer) => {
const client = new ProductClient(mockServer.url, 'valid-token');
await expect(client.getProduct('NONEXISTENT'))
.rejects.toThrow();
});
});
});
});
Python with Pact
from pact import Consumer, Provider
pact = Consumer('InventoryDashboard').has_pact_with(
Provider('ProductService'),
pact_dir='./pacts'
)
def test_get_product():
expected_body = {
"id": Term(r'^[a-f0-9-]{36}$', "550e8400-e29b-41d4-a716-446655440000"),
"name": Like("Blue Widget"),
"price": Like(29.99),
"category": Term(r'^(electronics|clothing|food|other)$', "electronics"),
}
(pact
.given("product exists")
.upon_receiving("a request for a product")
.with_request("GET", "/api/v2/products/550e8400-e29b-41d4-a716-446655440000")
.will_respond_with(200, body=expected_body))
with pact:
result = ProductClient(pact.uri).get_product(
"550e8400-e29b-41d4-a716-446655440000"
)
assert result["name"] is not None
assert result["price"] >= 0
Provider Verification
The provider team runs verification to ensure they meet all consumer expectations:
# Provider verification (runs against the real provider)
from pact import Verifier
verifier = Verifier(
provider='ProductService',
provider_base_url='http://localhost:8080'
)
# Set up provider states
@verifier.provider_state('product ABC-123 exists')
def setup_product_exists():
db.insert_product(id='ABC-123', name='Blue Widget', price=29.99)
@verifier.provider_state('product NONEXISTENT does not exist')
def setup_product_not_exists():
db.delete_product(id='NONEXISTENT')
# Verify all pacts from the broker
success = verifier.verify_with_broker(
broker_url='https://pact-broker.example.com',
publish_verification_results=True,
provider_version='1.2.3'
)
assert success
Pact in CI/CD
Consumer CI:
1. Run consumer Pact tests → generates Pact file
2. Publish Pact to broker
3. Check "can I deploy?" against broker
Provider CI:
1. Pull consumer Pacts from broker
2. Run provider verification
3. Publish verification results
4. Check "can I deploy?" against broker
The broker tracks which consumer versions are verified against which provider versions, enabling safe independent deployments.
Key Takeaway
Pact's consumer-driven approach ensures that API changes do not break consumers -- a problem that traditional API testing misses because providers test in isolation. AI dramatically accelerates Pact adoption by generating consumer contracts from actual client code, eliminating the biggest barrier to entry: the manual contract-writing effort.