Contract testing verifies that services can communicate correctly. It tests API contracts without spinning up dependent services.
What is Contract Testing?
Contract testing validates that a provider service meets the expectations of consumer services. Unlike integration tests, contract tests are fast and isolated.
Pact
Consumer Side
typescript
// consumer/consumer.test.ts
import { Pact } from '@pact-foundation/pact';
import { API } from './api';
describe('API Consumer Pact', () => {
const provider = new Pact({
consumer: 'OrderService',
provider: 'UserService',
port: 1234,
log: './logs/pact.log',
dir: './pacts'
});
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
afterEach(() => provider.verify());
test('get user by ID', async () => {
const expectedUser = {
id: '123',
name: 'John Doe',
email: 'john@example.com'
};
await provider.addInteraction({
state: 'user exists',
uponReceiving: 'a request for user',
withRequest: {
method: 'GET',
path: '/api/users/123'
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: expectedUser
}
});
const api = new API('http://localhost:1234');
const user = await api.getUser('123');
expect(user).toEqual(expectedUser);
});
});Provider Side
typescript
// provider/provider.test.ts
import { Verifier } from '@pact-foundation/pact';
import { server } from './server';
describe('API Provider Pact', () => {
let app: any;
beforeAll(async () => {
app = await server.start();
});
afterAll(() => server.stop());
test('validates consumer expectations', async () => {
const verifier = new Verifier({
providerBaseUrl: 'http://localhost:3000',
pactUrls: ['./pacts/orderservice-userservice.json'],
provider: 'UserService',
stateHandlers: {
'user exists': () => {
// Set up test data
return Promise.resolve();
}
}
});
await verifier.verify();
});
});Spring Cloud Contract
Producer
groovy
// BeerService.groovy
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description '''
Validates getting beer by ID
'''
request {
method GET()
url $(consumer(regex('/beers/[0-9]+')), producer('/beers/1'))
}
response {
status 200
body([
id: $(consumer(1), producer(anyNumber())),
name: $(consumer('Test Beer'), producer(anyNonEmptyString())),
type: 'IPA'
])
headers {
contentType(applicationJson())
}
}
}Consumer Test
groovy
// BeerServiceSpec.groovy
class BeerServiceSpec extends Specification {
@AutoStubWirePort
int stubPort
def 'should get beer by id'() {
given:
def client = new BeerClient("localhost:${stubPort}")
when:
def beer = client.getBeer(1)
then:
beer.id == 1
beer.name == 'Test Beer'
beer.type == 'IPA'
}
}Benefits
- Fast Tests: No need to spin up dependencies
- Fail Fast: Contract failures caught early
- Documentation: Contracts serve as documentation
- Independent Development: Teams work in parallel
Best Practices
- 1Version your contracts
- 2Store contracts in git
- 3Run in CI/CD pipeline
- 4Update contracts collaboratively
- 5Test both consumer and provider
Conclusion
Contract testing enables confident microservices development. Catch integration issues early without slow integration tests.