Contract Testing Replaces the Worst E2E Tests You've Ever Written
Microservices made the “run the whole stack to test it” approach intractable — too slow, too flaky, too much shared infrastructure. Contract testing flips the model: each consumer declares what it expects from a provider, and each provider verifies it meets those expectations. No shared environment, no end-to-end orchestration, full deploy independence — if both sides green, they're compatible.
- Deploying service A breaks service B in production, and your “integration env” missed it
- Teams freeze deploys around release windows to avoid cross-service breakage
- You have a 200-test E2E suite that takes 40 minutes and tells you very little
- API specs drift from reality between versions
Consumer-Driven Contracts with Pact give you the breakage signal early, in CI, on each side of the boundary independently.
How Pact Works
flowchart LR
subgraph Consumer
CT[Consumer test<br/>describes expected request/response]
CT -->|generates| PACT[(pact.json)]
end
PACT -->|publish| BROKER[Pact Broker / PactFlow]
subgraph Provider
BROKER -->|fetch| PV[Provider verifies<br/>against running service]
PV -->|results| BROKER
end
BROKER -->|can-i-deploy?| DEPLOY[CD pipeline]
The Consumer Side
On the consumer, you write a test against a Pact mock server. The test exercises your real client code; Pact captures every interaction (request + expected response) into a JSON pact file.
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike } = MatchersV3;
const provider = new PactV3({
consumer: 'web-checkout',
provider: 'orders-api',
dir: './pacts',
});
it('GET /orders/:id returns an order', async () => {
await provider
.given('an order with id 123 exists')
.uponReceiving('a request for order 123')
.withRequest({ method: 'GET', path: '/orders/123' })
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: like({
id: '123',
status: 'paid',
items: eachLike({ sku: 'X', qty: 1, price: 9.99 }),
}),
})
.executeTest(async (mock) => {
const client = new OrdersClient(mock.url);
const order = await client.getOrder('123');
expect(order.status).toBe('paid');
});
});Notice the matchers: like and eachLike describe shape, not exact values. The contract says “there's a numeric qty”, not “qtyis exactly 1”. The provider is free to return any value of the right type.
The Provider Side
The provider runs its real service against the pact file and replays each interaction. Provider states (the given clauses) are set up by handlers that seed the database appropriately before each verification.
import { Verifier } from '@pact-foundation/pact';
await new Verifier({
provider: 'orders-api',
providerBaseUrl: 'http://localhost:3000',
pactBrokerUrl: 'https://pact.example.com',
publishVerificationResult: true,
providerVersion: process.env.GIT_SHA,
consumerVersionSelectors: [
{ mainBranch: true }, // verify against consumers' main
{ deployedOrReleased: true }, // and what's in prod / pre-prod
],
stateHandlers: {
'an order with id 123 exists': async () =>
db.orders.insert({ id: '123', status: 'paid', items: [...] }),
},
}).verifyProvider();can-i-deploy — The Safety Gate
The Broker tracks which versions of which services are compatible. Before deploying, ask the Broker:
# Block the deploy unless this version is verified against all
# deployed consumers (and provider, if we're the consumer side)
pact-broker can-i-deploy \
--pacticipant orders-api \
--version "$GIT_SHA" \
--to-environment productionThis is where contract testing earns its keep — it turns “is this safe to ship” from a judgment call into a yes/no answer from the system.
Bidirectional Contract Testing
Pact's 2024+ Bi-Directional feature lets the provider use any spec source (OpenAPI doc, generated schema) instead of literally executing the pacts. The Broker checks consumer expectations against the provider's spec. This is the realistic path when the provider team can't adopt Pact (legacy, vendor, etc.).
When Contract Testing Helps Most
- Microservices owned by different teams with independent deploys.
- Public or partner APIs where you can't coordinate releases.
- Mobile and SPA clients consuming a backend — Pact pacts replace the “we changed the API and broke iOS” surprise.
- Replacing flaky end-to-end happy-path tests that only verify the API contract.
When It Doesn't Help
- Monoliths with one deployable. The CI test suite already covers everything.
- Event-driven systems with complex semantic guarantees (use schema registries + compatibility checks; Pact supports messaging but it's a different model).
- Business workflow correctness — contracts verify the wire format, not the meaning. You still need E2E or end-state tests for that.
What Each Layer Catches
| Layer | Catches | Misses |
|---|---|---|
| Unit | Logic bugs in one component | Interaction bugs |
| Integration | DB / framework wiring | Cross-service contracts |
| Contract | Wire-format breakage between services | Semantic correctness, business flows |
| E2E | Whole-system flows, UI behavior | Slow, flaky, expensive |
Tooling Beyond Pact
- Spring Cloud Contract — Pact-like for the Java/Spring ecosystem.
- OpenAPI + Schemathesis / Dredd — schema-first; consumer-blind but useful if you have a strong API-first culture.
- Buf / Protobuf compatibility — for gRPC, schema evolution checks at compile time.
- JSON Schema + AsyncAPI — for event payloads on a message bus.
Pitfalls
Over-specifying contracts
If the consumer asserts on the exact UUID format, an order ID change breaks the pact even though no real consumer cares. Use matchers liberally. Specify what you depend on; don't snapshot the entire response.
Provider states that drift
Provider state handlers must remain in sync with the consumer's assumptions. When the consumer changes “an order exists” to “an order in state paid exists”, the provider handler must also change. Treat state handlers as part of the API surface.
Ignoring can-i-deploy in CD
Adopting Pact without wiring can-i-deploy into the deploy pipeline gives you most of the cost and little of the benefit. The broker check is what enforces the contract in practice.

