The Microservices Integration Problem
In microservices architecture, services communicate with each other. But what happens when the user service changes its API contract and breaks the order service? Contract testing is your safety net.
Traditional integration tests are slow and fragile:
- You need all services running
- Tests are end-to-end and slow
- Failures are hard to diagnose
- Tests are coupled to implementation details
Contract testing solves this by defining explicit agreements between services.
What is a Contract?
A contract is an agreement about:
- What endpoints exist
- What data they accept
- What data they return
- What error conditions are possible
Example contract: "The Order Service calls the User Service's GET /users/:id endpoint and expects a 200 response with {id, name, email}."
Consumer vs Provider
In contract testing, we have two sides:
Consumer - The service that makes the API call (Order Service)
Provider - The service that provides the API (User Service)
Each writes tests from their perspective:
- Consumer tests: "Does the User Service respond as expected?"
- Provider tests: "Do my responses match what consumers expect?"
PACT Framework
PACT is the most popular contract testing framework. It has implementations for Node.js, Java, Python, Go, and more.
How PACT works:
- Consumer writes expectations for the provider
- Test runs against a mock provider
- Consumer generates a contract file
- Provider reads the contract and tests against it
- Both tests pass = compatible
Consumer Contract Example
import { pactWith } from 'jest-pact';
pactWith(
{ consumer: 'OrderService', provider: 'UserService' },
(interaction) => {
describe('GET /users/:id', () => {
it('returns a user', () =>
interaction
.given('user 123 exists')
.uponReceiving('a request for user 123')
.withRequest({
method: 'GET',
path: '/users/123'
})
.willRespondWith({
status: 200,
body: {
id: 123,
name: 'John Doe',
email: 'john@example.com'
}
})
.executeTest((mockProvider) => {
return userService.getUser(123);
})
);
});
}
);
CI/CD Integration
Contract testing shines in CI/CD:
- Consumer builds and generates contract
- Contract is published to a broker
- Provider downloads contract and tests
- If provider passes, deployment is safe
- If provider fails, it blocks deployment
Real-World Scenario
Imagine your Order Service calls User Service for user data. The contract specifies:
GET /users/123
Response: { id: 123, name: 'John', email: 'john@example.com' }
Now User Service wants to add a createdAt field. With contract testing:
- User Service adds the field (backward compatible, contract still passes)
- Order Service continues working without changes
- No integration test needed
But if User Service removes the id field:
- Provider test fails (breaks the contract)
- Deployment is blocked
- Team gets notified immediately
Conclusion
Contract testing prevents integration surprises in microservices. It's faster than integration tests, provides immediate feedback, and documents service agreements explicitly. If you have more than one service, contract testing should be part of your testing strategy.