Why Contract Testing?

In microservices, services are developed and deployed independently. Without contract testing, you rely on:

  • Integration tests: Require all services running simultaneously — slow and fragile.
  • Documentation: Developers read API docs and hope they are accurate.
  • Hope: Deploy and pray nothing breaks.

Contract testing fills the gap by verifying that services can communicate correctly without needing all services to be running at the same time.

How Pact Works

Pact is the most popular contract testing framework. It uses a consumer-driven approach:

1. Consumer writes tests defining what it expects from the provider
2. Pact generates a contract file (JSON) from these tests
3. Contract is shared with the provider (via Pact Broker or file)
4. Provider runs the contract against its actual implementation
5. If the provider satisfies all contracts, it is safe to deploy

The Pact Lifecycle

Consumer CI:                    Provider CI:
┌─────────────────┐            ┌─────────────────┐
│ Run consumer     │            │ Fetch contracts  │
│ tests against    │            │ from Pact Broker │
│ Pact mock        │            │                  │
│        │         │            │ Run provider     │
│        ▼         │            │ against each     │
│ Generate .pact   │───────────▶│ contract         │
│ file             │  Publish   │        │         │
│        │         │  to Broker │        ▼         │
│        ▼         │            │ Pass: safe to    │
│ Publish to       │            │ deploy           │
│ Pact Broker      │            │ Fail: fix before │
│                  │            │ deploying        │
└─────────────────┘            └─────────────────┘

Writing Consumer Tests

The consumer defines interactions it expects with the provider:

// consumer.test.js — User Service consumer test
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like, eachLike, string, integer } = MatchersV3;

const provider = new PactV3({
  consumer: 'OrderService',
  provider: 'UserService',
});

describe('UserService contract', () => {
  test('get user by ID', async () => {
    // Define expected interaction
    await provider
      .given('user with ID 123 exists')
      .uponReceiving('a request for user 123')
      .withRequest({
        method: 'GET',
        path: '/users/123',
        headers: { Accept: 'application/json' },
      })
      .willRespondWith({
        status: 200,
        headers: { 'Content-Type': 'application/json' },
        body: {
          id: integer(123),
          name: string('Alice'),
          email: string('alice@example.com'),
        },
      });

    await provider.executeTest(async (mockServer) => {
      // Call your actual consumer code against the mock
      const userClient = new UserClient(mockServer.url);
      const user = await userClient.getUser(123);

      expect(user.id).toBe(123);
      expect(user.name).toBeDefined();
      expect(user.email).toContain('@');
    });
  });

  test('user not found returns 404', async () => {
    await provider
      .given('user with ID 999 does not exist')
      .uponReceiving('a request for non-existent user 999')
      .withRequest({
        method: 'GET',
        path: '/users/999',
      })
      .willRespondWith({
        status: 404,
        body: { error: string('User not found') },
      });

    await provider.executeTest(async (mockServer) => {
      const userClient = new UserClient(mockServer.url);
      await expect(userClient.getUser(999)).rejects.toThrow('User not found');
    });
  });
});

This test runs against a Pact mock server (not the real provider). It generates a contract file describing the expected interactions.

Writing Provider Tests

The provider verifies it can fulfill all consumer contracts:

// provider.test.js — User Service provider verification
const { Verifier } = require('@pact-foundation/pact');

describe('UserService provider verification', () => {
  test('verifies all consumer contracts', async () => {
    const verifier = new Verifier({
      providerBaseUrl: 'http://localhost:3001', // Your actual provider
      pactBrokerUrl: 'https://your-broker.pact.io',
      provider: 'UserService',
      providerVersion: process.env.GIT_COMMIT,
      publishVerificationResult: true,

      // State handlers — set up test data for each "given" state
      stateHandlers: {
        'user with ID 123 exists': async () => {
          await db.users.create({ id: 123, name: 'Alice', email: 'alice@example.com' });
        },
        'user with ID 999 does not exist': async () => {
          await db.users.deleteAll();
        },
      },
    });

    await verifier.verifyProvider();
  });
});

Pact Matchers

Pact uses matchers to make contracts flexible. Instead of requiring exact values, matchers define the expected shape:

MatcherPurposeExample
like(value)Matches type, not exact valuelike(42) matches any integer
string(value)Matches any stringstring('Alice') matches any string
integer(value)Matches any integerinteger(1) matches any integer
eachLike(value)Array where each element matcheseachLike({id: integer()})
regex(pattern, example)Matches regex patternregex('^\\d{3}$', '123')
datetime(format, example)Matches date formatISO 8601 date

Pact Broker

The Pact Broker is a central service for managing contracts:

# Start Pact Broker with Docker
docker run -d --name pact-broker \
  -e PACT_BROKER_DATABASE_URL=sqlite:////tmp/pact_broker.sqlite3 \
  -p 9292:9292 \
  pactfoundation/pact-broker

Can I Deploy? The Broker tracks which versions have been verified. The can-i-deploy tool answers: “Is it safe to deploy this version?”

# Check if OrderService v1.2.3 can be deployed
pact-broker can-i-deploy \
  --pacticipant OrderService \
  --version 1.2.3 \
  --to production

Exercise: Contract Testing Implementation

Setup

Create two services and implement contract testing between them.

Task 1: Consumer Contract Tests

Write consumer tests for an OrderService that consumes a ProductService API:

Interactions to define:

  1. GET /products/:id — Returns product with name, price, and stock.
  2. GET /products?category=electronics — Returns array of products.
  3. POST /products/:id/reserve — Reserves stock, returns confirmation.
  4. GET /products/999 — Returns 404 for non-existent product.

Task 2: Provider Verification

Write provider tests for the ProductService:

  1. Set up state handlers for each “given” state.
  2. Start the actual ProductService.
  3. Run verification against consumer contracts.
  4. Ensure all interactions pass.

Task 3: Pact Broker Integration

  1. Set up a local Pact Broker with Docker.
  2. Publish consumer contracts to the Broker.
  3. Configure provider verification to fetch from the Broker.
  4. Use can-i-deploy to check deployment safety.

Task 4: Breaking Change Detection

  1. Make a breaking change in the provider (rename a field, change a status code).
  2. Run provider verification — it should fail.
  3. Document the failure message and how it helps identify the issue.
  4. Fix the breaking change or coordinate a contract update.

Task 5: CI Pipeline Integration

Design a CI pipeline that:

  1. Consumer CI: Run consumer tests → publish contract to Broker → check can-i-deploy.
  2. Provider CI: Fetch contracts from Broker → verify provider → publish results → check can-i-deploy.

Deliverables

  1. Consumer test code with at least 4 interactions.
  2. Provider verification code with state handlers.
  3. Pact Broker setup and configuration.
  4. CI pipeline design document.
  5. Breaking change detection demonstration.