Back to Blog

Contract Testing: Pact Consumer-Driven Contracts

Implement contract testing with Pact in 2026 — consumer-driven contract tests in TypeScript, provider verification, Pact Broker setup, CI pipeline integration,

Viprasol Tech Team
12 min read
Updated 2026

Contract Testing: Pact Consumer-Driven Contracts, Provider Verification, and CI Integration

Quick answer. Contract testing replaces fragile shared-environment integration tests: the consumer writes a test recording what it expects, generating a Pact file published to a Pact Broker. The provider then verifies independently that it honors that contract. Neither service needs the other running, making boundary tests fast and reliable in CI.

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 1000+ 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:

testing - Contract Testing: Pact Consumer-Driven Contracts

🚀 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?"


How Viprasol Helps

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.


Related Topics

testingpactmicroservicesapidevops
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 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.

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.