What is consumer-driven contract testing? How does Pact work?
60-second answer
Contract testing checks that two services agree on the shape of their interactions, without deploying them together. In consumer-driven contracts, the consumer writes tests describing the requests it makes and the responses it needs; Pact runs those against a mock provider and records them into a contract file, the pact. The pact is published to a Pact Broker. The provider then verifies the pact against its real implementation in its own pipeline. can-i-deploy checks the broker before release, so a provider change that would break a consumer is caught before it's deployed.
Explanation
Benefits over E2E: fast, isolated, pinpoints the breaking change, scales with many services.
Key concepts:
- Interaction: given (provider state), upon receiving (request), will respond with (response).
- Matchers: match types and formats, not exact values, so contracts aren't brittle.
- Provider states: setup hooks so the provider has the right data.
- Broker + can-i-deploy + webhooks to trigger provider verification when a pact changes.
- Contract tests check the interface, not business logic; functional tests are still needed.
Example
The web app (consumer) expects GET /orders/{id} to return status as a string enum. The orders team renames it to orderStatus. Provider verification fails in their PR, before anything reaches staging.
Sample code
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
const { like, regex } = MatchersV3;
const provider = new PactV4({ consumer: 'web-app', provider: 'orders-service' });
it('gets an order', () =>
provider
.addInteraction()
.given('order ORD-1 exists')
.uponReceiving('a request for order ORD-1')
.withRequest('GET', '/orders/ORD-1')
.willRespondWith(200, (b) =>
b.jsonBody({ id: 'ORD-1', status: regex('PLACED|PAID|SHIPPED', 'PAID'), total: like(999.0) }))
.executeTest(async (mock) => {
const order = await new OrdersClient(mock.url).get('ORD-1');
expect(order.status).toBe('PAID');
}));