Back to Blog

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,

Viprasol Tech Team
June 22, 2026
12 min read

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

ScenarioE2E TestContract 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

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol · Web Development

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.