Introduction
Contract testing verifies that services can communicate without running full integration tests. Each service’s contract is verified against its consumers or providers. This guide covers Pact and contract testing patterns.
The Problem
┌─────────────────────────────────────────────────────────────┐
│ Without Contract Testing │
├─────────────────────────────────────────────────────────────┤
│ │
│ Service A ────────── Service B │
│ │ │ │
│ │ Team develops │ Team develops │
│ │ Independently │ Independently │
│ │ │ │
│ │ Deploy ──────▶│ Integration! │
│ │ │ │
│ │ Bugs found! │
│ │ Hotfix needed! │
│ │ Deploy delayed! │
│ │
└─────────────────────────────────────────────────────────────┘
Pact Overview
┌─────────────────────────────────────────────────────────────┐
│ Contract Testing Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ Consumer (Test) Provider (Verify) │
│ ───────────── ────────────── │
│ │ │ │
│ 1. Write test ──────▶ 3. Verify │
│ (defines (against │
│ expectations) recorded │
│ contracts) │
│ │ │ │
│ 2. Record ────────────▶ 4. Results │
│ contract (Pass/Fail) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Pact Broker │ │
│ │ Stores and shares contracts │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Consumer-Side Testing
Setup
npm install @pact-foundation/pact
Writing Consumer Tests
import { Pact } from '@pact-foundation/pact';
import { describe, it, expect } from '@playwright/test';
describe('User Service Consumer', () => {
const pact = new Pact({
consumer: 'web-app',
provider: 'user-service',
cors: true,
});
beforeAll(() => pact.setup());
afterAll(() => pact.finalize());
it('should get user by ID', async () => {
// Define expected interaction
await pact.addInteraction({
uponReceiving: 'a request for user by ID',
withRequest: {
method: 'GET',
path: '/api/users/123',
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: '123',
name: 'John Doe',
email: '[email protected]',
},
},
});
// Execute test
const response = await fetch('http://localhost:3001/api/users/123');
const user = await response.json();
expect(response.status).toBe(200);
expect(user.name).toBe('John Doe');
// Verify contract
await pact.verify();
});
});
Provider-Side Testing
Setup
npm install @pact-foundation/pact-node
Verifying Provider
import { Verifier } from '@pact-foundation/pact-node';
const verifier = new Verifier({
provider: 'user-service',
providerBaseUrl: 'http://localhost:3001',
// Load contracts from broker
pactBrokerUrl: 'https://pact-broker.example.com',
pactBrokerToken: process.env.PACT_TOKEN,
// Or load from file
// pactUrls: ['./contracts/user-service-web-app.json'],
publishVerificationResult: true,
providerVersion: '1.0.0',
});
verifier.verifyProvider().then(() => {
console.log('All contracts verified!');
});
Consumer-Driven Contracts
Pattern
// Consumer defines what it needs
const userContract = {
consumer: { name: 'web-app' },
provider: { name: 'user-service' },
interactions: [
{
description: 'get user by ID',
request: {
method: 'GET',
path: '/api/users/{userId}',
},
response: {
status: 200,
body: {
id: '{userId}',
name: 'string',
email: 'string(email)',
},
},
},
],
};
// Provider must satisfy this contract
Best Practices
practices:
- "Write contracts from consumer perspective"
- "Version contracts clearly"
- "Use Pact Broker for sharing"
- "Run provider verification in CI"
- "Test error responses too"
Key Takeaways
- Consumer tests - Define what you need from a service
- Provider tests - Verify you satisfy contracts
- Pact Broker - Share contracts between teams
- Essential for microservices - Independent deploys
Comments