Contract Testing: Pact Consumer-Driven Contracts, Provider Verification, and CI Integration
Implement contract testing with Pact in 2026 — consumer-driven contract tests in TypeScript, provider verification, Pact Broker setup, CI pipeline integration,
Contract Testing: Pact Consumer-Driven Contracts, Provider Verification, and CI Integration
Integration tests that spin up multiple services are fragile, slow, and expensive to maintain. Contract testing is a better approach for service boundaries: the consumer defines what it needs from the provider, and the provider verifies it can fulfill those needs — without either service needing to run the other.
This is the difference between "we tested it together in a shared test environment" (slow, flaky) and "each service proves independently that it honors the contract" (fast, reliable).
How Pact Works
1. Consumer writes a test that records what it expects from the provider
→ Generates a "pact file" (JSON) describing the interaction
2. Pact file is published to Pact Broker
→ Versioned, tagged, and accessible to providers
3. Provider pulls the pact file and runs verification
→ Plays back the recorded requests against the real provider
→ Confirms responses match what the consumer expected
4. Pact Broker records whether verification passed
→ CI can gate deployments: "can I deploy consumer@v2 with provider@main?"
No shared test environment. No Docker Compose with 5 services. Each service tests its side independently.
Consumer Test (TypeScript)
The consumer test defines an interaction: "when I call this endpoint with this request, I expect this response."
// tests/orders-api.consumer.pact.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { resolve } from 'path';
import { OrdersApiClient } from '../src/lib/orders-api-client';
const { like, eachLike, string, integer, datetime } = MatchersV3;
const provider = new PactV3({
consumer: 'web-frontend',
provider: 'orders-api',
dir: resolve(process.cwd(), 'pacts'), // Where pact files are written
logLevel: 'warn',
});
describe('Orders API — Consumer Pact Tests', () => {
describe('GET /api/orders/:id', () => {
it('returns an order when it exists', async () => {
await provider
.given('order 123 exists for user abc')
.uponReceiving('a request for order 123')
.withRequest({
method: 'GET',
path: '/api/orders/order-123',
headers: {
Authorization: like('Bearer eyJhbGciOiJIUzI1NiJ9.xxx'),
Accept: 'application/json',
},
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: string('order-123'),
userId: string('user-abc'),
status: string('PENDING'),
totalCents: integer(4999),
createdAt: datetime("yyyy-MM-dd'T'HH:mm:ss.SSSX", '2026-06-01T10:00:00.000Z'),
items: eachLike({
productId: string('prod-xyz'),
quantity: integer(1),
priceCents: integer(4999),
}),
},
})
.executeTest(async (mockServer) => {
// Consumer code runs against Pact's mock provider
const client = new OrdersApiClient(mockServer.url);
const order = await client.getOrder('order-123', 'Bearer eyJhbGciOiJIUzI1NiJ9.xxx');
expect(order.id).toBe('order-123');
expect(order.status).toBe('PENDING');
expect(order.totalCents).toBe(4999);
expect(order.items).toHaveLength(1);
});
});
it('returns 404 when order does not exist', async () => {
await provider
.given('order 999 does not exist')
.uponReceiving('a request for non-existent order 999')
.withRequest({
method: 'GET',
path: '/api/orders/order-999',
headers: { Authorization: like('Bearer token') },
})
.willRespondWith({
status: 404,
body: { error: string('Order not found') },
})
.executeTest(async (mockServer) => {
const client = new OrdersApiClient(mockServer.url);
await expect(client.getOrder('order-999', 'Bearer token'))
.rejects.toThrow('Order not found');
});
});
});
describe('POST /api/orders', () => {
it('creates an order', async () => {
await provider
.given('user abc has sufficient balance')
.uponReceiving('a request to create an order')
.withRequest({
method: 'POST',
path: '/api/orders',
headers: {
Authorization: like('Bearer token'),
'Content-Type': 'application/json',
},
body: {
items: eachLike({ productId: string('prod-xyz'), quantity: integer(1) }),
},
})
.willRespondWith({
status: 201,
body: {
id: string('order-new'),
status: string('PENDING'),
totalCents: integer(4999),
},
})
.executeTest(async (mockServer) => {
const client = new OrdersApiClient(mockServer.url);
const order = await client.createOrder(
{ items: [{ productId: 'prod-xyz', quantity: 1 }] },
'Bearer token',
);
expect(order.status).toBe('PENDING');
});
});
});
});
After this test runs, a pact file is generated at pacts/web-frontend-orders-api.json.
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Provider Verification (orders-api service)
The provider pulls pact files from the Pact Broker and verifies it can fulfill each interaction:
// tests/orders-api.provider.pact.test.ts
import { Verifier } from '@pact-foundation/pact';
import { resolve } from 'path';
import { startServer, stopServer } from '../src/server';
import { db } from '../src/lib/db';
describe('Orders API — Provider Pact Verification', () => {
let app: any;
beforeAll(async () => {
app = await startServer(0); // Start on random port
});
afterAll(async () => {
await stopServer(app);
});
it('validates pacts from Pact Broker', async () => {
const verifier = new Verifier({
provider: 'orders-api',
providerBaseUrl: `http://localhost:${app.port}`,
// Fetch pacts from Pact Broker
pactBrokerUrl: process.env.PACT_BROKER_URL,
pactBrokerToken: process.env.PACT_BROKER_TOKEN,
consumerVersionSelectors: [
{ mainBranch: true }, // Test against consumer's main branch
{ deployedOrReleased: true }, // Test against deployed consumer versions
],
// Provider states — set up database state before each interaction
stateHandlers: {
'order 123 exists for user abc': async () => {
await db.orders.upsert({
where: { id: 'order-123' },
create: {
id: 'order-123',
userId: 'user-abc',
status: 'PENDING',
totalCents: 4999,
createdAt: new Date('2026-06-01T10:00:00.000Z'),
items: {
create: [{ productId: 'prod-xyz', quantity: 1, priceCents: 4999 }],
},
},
update: { status: 'PENDING' },
});
},
'order 999 does not exist': async () => {
await db.orders.deleteMany({ where: { id: 'order-999' } });
},
'user abc has sufficient balance': async () => {
await db.users.upsert({
where: { id: 'user-abc' },
create: { id: 'user-abc', balanceCents: 10000 },
update: { balanceCents: 10000 },
});
},
},
// Tell Pact Broker this provider version was verified
publishVerificationResult: true,
providerVersion: process.env.GIT_SHA ?? 'local',
providerVersionBranch: process.env.GIT_BRANCH ?? 'local',
});
await verifier.verifyProvider();
});
});
Pact Broker Setup
Pact Broker stores pact files, tracks verifications, and powers the can-i-deploy check:
# docker-compose.pact-broker.yml
services:
pact-broker:
image: pactfoundation/pact-broker:latest
ports:
- "9292:9292"
environment:
PACT_BROKER_DATABASE_URL: postgresql://pact:pact@postgres/pact
PACT_BROKER_BASIC_AUTH_USERNAME: admin
PACT_BROKER_BASIC_AUTH_PASSWORD: ${PACT_BROKER_PASSWORD}
depends_on:
- postgres
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: pact
POSTGRES_PASSWORD: pact
POSTGRES_DB: pact
volumes:
- pact_data:/var/lib/postgresql/data
volumes:
pact_data:
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
CI Pipeline Integration
# .github/workflows/consumer.yml — web-frontend CI
name: Consumer Tests
on: [push]
jobs:
pact-consumer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install
- name: Run consumer pact tests
run: pnpm test:pact
# Generates pact files in ./pacts/
- name: Publish pacts to Broker
run: |
pnpm pact-broker publish ./pacts \
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--broker-token ${{ secrets.PACT_BROKER_TOKEN }} \
--consumer-app-version ${{ github.sha }} \
--branch ${{ github.ref_name }}
- name: Can I deploy? (check if provider verified this pact)
run: |
pnpm pact-broker can-i-deploy \
--broker-base-url ${{ secrets.PACT_BROKER_URL }} \
--broker-token ${{ secrets.PACT_BROKER_TOKEN }} \
--pacticipant web-frontend \
--version ${{ github.sha }} \
--to-environment production
# Fails if provider hasn't verified the pacts yet
# .github/workflows/provider.yml — orders-api CI
name: Provider Verification
on:
push:
# Also trigger when consumer publishes a new pact
repository_dispatch:
types: [pact-changed]
jobs:
pact-provider:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env: { POSTGRES_DB: test, POSTGRES_USER: test, POSTGRES_PASSWORD: test }
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm install
- name: Verify pacts from Broker
env:
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
GIT_SHA: ${{ github.sha }}
GIT_BRANCH: ${{ github.ref_name }}
run: pnpm test:pact:provider
# Fetches pacts, runs state handlers, verifies responses
When Contract Tests Replace E2E Tests
| Scenario | E2E Test | Contract Test |
|---|---|---|
Frontend calls /api/orders/123 correctly | ✅ | ✅ faster |
| Orders API returns correct format | ✅ | ✅ faster |
| Frontend and API work together end-to-end | ✅ | ❌ not the right tool |
| Business flow (create order → charge payment) | ✅ | ❌ |
| Service boundary contract is stable | ❌ fragile | ✅ |
| Detecting breaking API changes before deploy | ✅ slow | ✅ fast |
The right balance: Keep a small suite of E2E tests for critical business flows. Use contract tests to replace the large surface area of "does service A call service B correctly?"
Working With Viprasol
We set up contract testing infrastructure for microservice architectures — Pact consumer and provider tests, Pact Broker deployment, CI integration with can-i-deploy gates, and migration strategies from brittle integration test suites.
→ Talk to our team about testing strategy and microservice architecture.
See Also
- Testing Strategy — where contract testing fits in the pyramid
- CI/CD Pipeline Setup — CI workflows for microservices
- API Gateway Comparison — managing API contracts at the gateway
- OpenAPI Design — API-first design complements contract testing
- Web Development Services — testing and quality engineering
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.