Contract Testing
Pact, consumer-driven contracts, mock services, integration test strategies
Contract testing verifies that a consumer and provider agree on the API contract without requiring a full end-to-end integration environment. In consumer-driven contract testing with Pact, consumers define their expectations (request shape, response fields, status codes) as contracts; the provider runs these contracts against its actual implementation, ensuring it fulfills all consumer expectations. This enables teams to deploy services independently with confidence that integration will work, replacing slow and flaky E2E integration test suites with fast, isolated contract verification.
Key Points
- Consumer-driven contracts: the consumer writes the contract (what I will send and what I expect back); this is more useful than provider-driven contracts because consumers define the actual coupling.
- Pact workflow: consumer generates a pact file (JSON contract) during unit tests using Pact library; pact file published to Pact Broker; provider fetches pact from broker and verifies it against the real service in CI.
- Pact Broker: central store for pact contracts; tracks which consumer version is compatible with which provider version; provides can-i-deploy tool — checks if it is safe to deploy a new version given all published contracts.
- Contract vs integration test: contract test is fast (<1s, no network), deterministic, and catches breaking changes at the contract level; integration test uses real infrastructure but is slow, flaky, and expensive to maintain.
- Provider states: Pact allows specifying pre-conditions ("given: user 42 exists") that the provider sets up before verifying each interaction — enables testing both happy and error paths.
- gRPC contract testing: Pact supports protobuf-encoded gRPC contracts; alternatively, buf.build enforces protobuf compatibility rules (BACKWARD, FORWARD, FULL) at the schema level before code generation.
- OpenAPI contract testing: tools like Dredd (Apiary) and Schemathesis run property-based tests against a live server using the OpenAPI spec as the contract — different from Pact (spec-first vs consumer-first).
- Breaking change detection: any change to the consumer-visible API contract (removing a field, changing a type, renaming a field) will fail the provider verification step in CI — acts as a safety net for accidental breaking changes.
Consumer-driven contract test with Pact V3 — defines expected interaction, verifies against mock, publishes contract to Pact Broker
// Consumer test (TypeScript + Pact)
describe('OrderService consumer', () => {
const provider = new PactV3({
consumer: 'PaymentService',
provider: 'OrderService',
});
it('gets an order by ID', async () => {
await provider
.given('order 123 exists')
.uponReceiving('a GET request for order 123')
.withRequest({ method: 'GET', path: '/orders/123' })
.willRespondWith({
status: 200,
body: MatchersV3.like({
id: '123',
total: MatchersV3.number(99.99),
status: MatchersV3.string('PLACED'),
}),
})
.executeTest(async (mockServer) => {
const order = await orderClient.getOrder('123', mockServer.url);
expect(order.id).toBe('123');
});
});
});
// Pact file published → Provider CI fetches and verifies against real OrderServiceReal-World Example
Atlassian uses Pact for contract testing across their microservices (Jira, Confluence, Trello integrations), publishing thousands of pact files to a central Pact Broker and using can-i-deploy as a deployment gate. ING Bank adopted consumer-driven contract testing to replace a 6-hour full integration test suite with sub-5-minute contract verification runs, enabling multiple daily deploys.