Q13 of 37 · API testing

What is contract testing (e.g. Pact) and when do you use it?

API testingMidapicontract-testingpactmicroservices

Short answer

Short answer: Contract testing pins the agreement between a consumer (frontend, mobile, microservice client) and a provider (API). The consumer records expected interactions; the provider replays them in CI to catch breaking changes. Use it when you have multiple independent services and end-to-end testing across all of them is too slow.

Detail

The problem contract testing solves: in microservices, an integration bug between Service A and Service B traditionally requires running both services together (or shared E2E tests). That's slow, brittle, and breaks down at scale. Contract tests replace "do they work together end-to-end?" with "does the consumer's expectations match what the provider promises?"

How Pact works (consumer-driven):

1. Consumer writes a test that records interactions:

const provider = new Pact({ consumer: 'WebApp', provider: 'UserService' });

provider.addInteraction({
  state: 'a user with id 42 exists',
  uponReceiving: 'a request for user 42',
  withRequest: { method: 'GET', path: '/users/42' },
  willRespondWith: {
    status: 200,
    body: { id: '42', email: like('alice@example.com') },
  },
});

When the consumer test runs, Pact stands up a mock provider on localhost, the real consumer code calls it, and the interaction is recorded as a pact file (JSON).

2. Pact file is published to a Pact Broker — the central registry of contracts.

3. Provider runs verification in its own CI:

# Provider CI fetches all consumer pacts and replays them
pact-verifier --provider=UserService --broker-url=https://broker.example.com

The provider must satisfy every recorded interaction. If the provider changes a field name and a consumer expects the old one, verification fails in the provider's CI — before the change is deployed.

When to use contract testing:

  • Multiple independent services owned by different teams.
  • Cross-team coordination is slow; you want each side to test in isolation but still catch integration breaks.
  • E2E test environments are flaky or expensive to maintain.

When NOT to use it:

  • Monolith with no separate consumers — contracts add ceremony with no payoff.
  • Public APIs with thousands of unknown consumers — Pact doesn't scale to "every customer." For public APIs, use OpenAPI + schema validation instead.
  • Teams that won't actually look at broken pacts — contract testing only works if it's blocking.

Pact ergonomics caveats: it has a learning curve, the broker is yet another piece of infra, and "consumer-driven" means consumers and providers must coordinate on naming and states. Worth it at scale; overkill for a 2-service team.

// EXAMPLE

user.consumer.pact.test.js

const { Pact, Matchers } = require('@pact-foundation/pact');
const { like, eachLike } = Matchers;

const provider = new Pact({
  consumer: 'WebApp',
  provider: 'UserService',
  port: 1234,
});

beforeAll(() => provider.setup());
afterEach(() => provider.verify());
afterAll(() => provider.finalize());

test('GET /users/42', async () => {
  await provider.addInteraction({
    state: 'user 42 exists',
    uponReceiving: 'a request for user 42',
    withRequest: { method: 'GET', path: '/users/42' },
    willRespondWith: {
      status: 200,
      body: { id: '42', email: like('a@x.com') },
    },
  });

  const res = await fetch('http://localhost:1234/users/42');
  expect(res.status).toBe(200);
});

// WHAT INTERVIEWERS LOOK FOR

Consumer/provider mental model, the Pact Broker as central registry, when to use vs OpenAPI for public APIs, and honesty about overhead — Pact isn't free.

// COMMON PITFALL

Treating Pact as a replacement for E2E tests. It catches contract breaks; it doesn't catch business-logic bugs. Both still matter — contract tests reduce *how many* E2E tests you need, not the need entirely.