23Contract Testing (Pact)
What you will master here
- What contract testing is and the problem it solves
- Consumer-driven contracts (CDC)
- How Pact works: consumer test → pact file → broker → producer verification
- Writing consumer tests (JS, Java)
- Provider verification
- Pact Broker
- Comparison vs traditional integration tests
23.1 The problem
Microservice A calls Microservice B. Traditional ways to test the integration:
- End-to-end tests — slow, flaky, expensive to maintain
- Shared mocks — drift from reality without anyone noticing
- Schema check (OpenAPI) — catches shape changes but not semantic mismatch
Contract testing: the consumer declares "I send X, expect Y back"; the producer verifies it can satisfy that contract. Producer changes that break the contract fail BEFORE deploy.
23.2 How Pact works
Figure — Consumer-Driven Contract flow.
23.3 Consumer test (Node)
// users-client.consumer.spec.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { like, eachLike, integer, string } = MatchersV3;
const provider = new PactV3({ consumer: 'web', provider: 'users-api' });
test('GET /users/42', async () => {
provider
.given('user 42 exists')
.uponReceiving('a request for user 42')
.withRequest({ method: 'GET', path: '/users/42',
headers: { Accept: 'application/json' } })
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: like({
id: integer(42),
email: string('alice@test.com'),
orders: eachLike({ id: string('o_1'), total: integer(99) }),
}),
});
await provider.executeTest(async (mockServer) => {
const client = new UsersClient(mockServer.url);
const user = await client.getUser(42);
expect(user.email).toBe('alice@test.com');
});
});
Result: a JSON pact file in pacts/web-users-api.json describing the interaction.
23.4 Publishing to the broker
npm i -D @pact-foundation/pact-node pact-broker publish ./pacts \ --consumer-app-version=$GIT_SHA \ --branch=$GIT_BRANCH \ --broker-base-url=https://pact-broker.acme.com
23.5 Provider verification (Java)
@Provider("users-api")
@PactBroker(url = "https://pact-broker.acme.com")
public class UsersApiPactTest {
@LocalServerPort int port;
@BeforeEach
void before(PactVerificationContext ctx) {
ctx.setTarget(new HttpTestTarget("localhost", port));
}
@State("user 42 exists")
void userExists() {
/* seed DB so the request will succeed */
userRepo.save(new User(42, "alice@test.com"));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
}
23.6 Pact Broker
Central service for pact storage and orchestration. Key features:
- Store every pact + version
- Webhooks: trigger provider verification when a new pact is published
- can-i-deploy — pre-deploy check: "can consumer X version Y safely deploy against current provider Y?" Fails if no successful verification exists.
- Network graph visualisation across all services
# Gate deploy pact-broker can-i-deploy --pacticipant web --version $GIT_SHA --to-environment production
23.7 Matchers (the magic)
You don't pin exact values — you specify shapes. Producer can return different ACTUAL values; only the shape must match.
| Matcher | Means |
|---|---|
like(example) | Same type as example |
eachLike([example]) | Array of elements matching example |
integer(42) | Any integer |
string('x') | Any string |
regex(/pattern/, example) | String matching regex |
timestamp('yyyy-MM-dd', example) | Date format |
23.8 Contract vs Schema vs E2E
| Approach | Catches | Misses |
|---|---|---|
| OpenAPI schema check | Type/structure changes | Semantic mismatch (right shape, wrong meaning) |
| Contract testing (Pact) | Real consumer/producer mismatch with actual examples | Bugs entirely within one service |
| End-to-end | Full system behaviour | Slow, flaky, expensive to maintain at scale |
Real-world setupUse ALL three layered: schema for cheapest checks, contracts to gate cross-service changes, a small E2E suite for critical user journeys. Contract testing replaces "many integration tests" not "all tests".
Module 49 — Pact Q&A
What problem does contract testing solve?
In a microservice architecture, producer changes can silently break consumers. End-to-end tests catch it but slowly. Contract testing catches it BEFORE deploy — consumer test pins the expectation, producer's CI verifies it.
What does "consumer-driven" mean?
The consumer (caller) writes the contract describing what it expects. The producer (callee) must satisfy it. Reverse of schema-first APIs where the producer defines first. Better aligns to actual consumer needs.
What is a Pact file?
JSON document describing one or more interactions: request shape, expected response shape, and provider states needed to satisfy them. Generated by the consumer test. Published to a broker.
What is the Pact Broker?
Central service that stores pacts, links consumer + provider versions, triggers verification webhooks, and answers "can I deploy?" via the can-i-deploy command. The brain of a Pact ecosystem.
What is can-i-deploy?
A broker query: "given this version of service X, is it safe to deploy to environment Y based on the latest verified pacts?" Returns yes/no. Wired into deploy pipelines as a hard gate.
What's a provider state?
A named precondition the producer must set up before replaying an interaction — e.g. "user 42 exists". The provider verification test registers a function for each state that seeds the data, then runs the interaction.
Why use matchers like like instead of pinning exact values?
You don't care about specific values, only shape/type. A consumer that pinned
id: 42 would break when producer returns id 43 — pointless flakes. Match shape, allow values to vary.Contract vs E2E — when each?
Contract for cross-service expectations — fast, cheap, runs in each service's CI independently. E2E for true user journeys through multiple services. Contract dramatically reduces the number of E2Es needed.
Pact vs Spring Cloud Contract?
Pact: consumer-driven, language-agnostic broker, mature ecosystem. Spring Cloud Contract: producer-driven (provider writes the contract), Spring-native, can also generate WireMock stubs for consumers. Pick by team conventions.
Does Pact support async/message queues?
Yes — Pact Message supports Kafka, RabbitMQ, etc. Consumer expectation is "given message X published, my handler does Y". Producer verifies it can produce a message matching that shape.