QA Engineer Skills 2026QA-2026Consumer-Driven Contract Testing with Pact

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:

  1. Analyzing client code to automatically identify API call patterns
  2. Generating consumer contracts from actual usage, not documentation
  3. Detecting contract drift when the provider changes
  4. 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 pattern
  • decimal -- value must be a decimal number
  • like -- 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.