Q13 of 37 · API testing
What is contract testing (e.g. Pact) and when do you use it?
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);
});