Contract Testing
Status: Complete
Category: Testing
Default enforcement: Advisory
Author: PushBackLog team
Tags
- Topic: testing, distributed-systems
- Skillset: backend
- Technology: generic
- Stage: execution, review
Summary
Contract testing verifies that a service provider and its consumers agree on the shape of their interface without requiring both to be running simultaneously. Each consumer publishes a “contract” — the requests it makes and the responses it expects — which the provider verifies it can satisfy. Contract testing replaces brittle cross-service E2E tests for integration points, enabling teams to verify compatibility independently and continuously.
Rationale
Cross-service E2E tests are expensive and fragile
Testing the integration between two services by running them both in a shared environment is the obvious approach, but it has serious costs: both services must be deployed and available simultaneously, test data must be shared and managed, and failures can originate from either service or from the shared infrastructure. These tests are slow, flaky, and hard to diagnose when they fail.
Consumer-driven contracts shift the integration risk left
Contract testing inverts the dependency: each consumer defines what it needs from the provider, and the provider verifies it can satisfy those contracts. The verification happens in isolation — no shared environment needed. This means consumers can verify compatibility with the provider’s current state without a joint deployment, and providers can detect which consumers would break before a change is released.
The most widely used contract testing tool is Pact, which implements consumer-driven contracts with a publish-verify workflow via a broker.
Guidance
How it works (Pact workflow)
1. Consumer team writes a consumer test that records interactions
→ Pact saves these as a "contract" (JSON pact file)
2. Contract is published to a Pact Broker
3. Provider CI pipeline pulls the pact and verifies it against the real provider
→ If the provider's response does not match the contract, the build fails
4. "Can I deploy?" check: Pact Broker reports whether consumer and provider
are compatible before any deployment
Consumer test (TypeScript / Pact)
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like } = MatchersV3;
const provider = new PactV3({
consumer: 'OrderService',
provider: 'PaymentService',
dir: './pacts',
});
describe('PaymentService contract', () => {
it('charges a card successfully', async () => {
await provider
.given('a valid card token exists')
.uponReceiving('a charge request')
.withRequest({
method: 'POST',
path: '/payments',
body: { amount: like(4999), currency: like('GBP'), token: like('tok_abc') },
})
.willRespondWith({
status: 201,
body: {
id: like('pay_xyz'),
status: like('succeeded'),
amount: like(4999),
},
});
const result = await provider.executeTest(async (mockServer) => {
const client = new PaymentClient(mockServer.url);
return client.charge({ amount: 4999, currency: 'GBP', token: 'tok_abc' });
});
expect(result.status).toBe('succeeded');
});
});
Provider verification
// Provider-side verification — runs in PaymentService CI
new Verifier({
provider: 'PaymentService',
providerBaseUrl: 'http://localhost:3001',
pactBrokerUrl: process.env.PACT_BROKER_URL,
publishVerificationResult: true,
providerVersion: process.env.GIT_SHA,
}).verifyProvider();
Matchers — what to match vs. what to verify
Contract tests use matchers rather than exact values wherever the exact value is not semantically meaningful:
| Matcher | Use when |
|---|---|
like(value) | Type and presence matter, not exact value |
eachLike(template) | Array of items matching the template |
regex(pattern, example) | Format matters (UUIDs, ISO dates, etc.) |
| Exact value | When a specific enum value, magic string, or exact code is semantically required |
What contract testing does and does not verify
Does verify:
- The response body shape matches the consumer’s expectations
- Required fields are present
- Field types are correct
- HTTP status codes match
Does not verify:
- Business logic correctness
- Performance under load
- State consistency across multiple calls
- Security — contract testing is not a substitute for auth testing
When to use contract testing
Contract testing is most valuable when:
- Multiple teams own different services that integrate via API
- You want to verify integration cheaply without a shared environment
- You need to detect when a provider change would break a consumer before deployment
For a monolith or single-team services, the overhead of contract testing may not be justified — integration tests within the same codebase are simpler.