Contract and Consumer-Driven Contract Tests
Validate API contracts between services to catch breaking changes before deployment.
TL;DR
Contract testing verifies API contracts between services. Consumer-driven contracts (CDC) start from the consumer's perspective: "What data do I need from the API?" Providers then implement that contract. Use Pact or similar tools to record contracts and replay them in CI/CD. CDC catches breaking changes before deployment—fast (mock provider) and focused (API contract only). Ideal for microservices architectures where API changes can break downstream services. Run consumer tests in CI to generate contract files; provider tests in CI to verify the contract; share contracts between services.
Learning Objectives
After reading this article, you will understand:
- The difference between contract testing and integration testing
- How consumer-driven contracts shift responsibility
- How to write consumer contract tests
- How to use Pact to record and validate contracts
- Best practices for managing contracts in microservices
- How to integrate contract testing into CI/CD pipelines
Motivating Scenario
Your company has 50+ microservices. Service A depends on Service B's /user/:id endpoint. Service B's team changes the response format (renames name to full_name). Service A still expects the old format; it crashes in production because no one caught the breaking change before deployment.
Contract testing prevents this: Service A (consumer) explicitly defines what it expects from Service B's API. When Service B changes the endpoint, the contract test fails in CI, preventing the breaking change from being deployed.
Core Concepts
Contract Testing vs. Integration Testing
| Aspect | Contract Testing | Integration Testing |
|---|---|---|
| Provider | Mock | Real |
| Speed | Fast (100s ms) | Slow (seconds) |
| Setup | Define contract | Start full system |
| Scope | API contract only | Full interaction |
| CI/CD Cost | Low | High |
| When to run | Every commit | Nightly or on release |
Consumer-Driven Contracts (CDC)
CDC flips the responsibility model:
Traditional (Provider-Driven):
- Provider team designs API
- Consumer team uses API
- Breaking changes might break consumers
Consumer-Driven (CDC):
- Consumer team defines needs: "I need GET /user/:id returning id, name, email"
- Provider team implements that contract
- Provider knows exactly what consumers need; harder to break contracts accidentally
Pact and Contract Recording
Pact is a CDC framework:
- Consumer test: Mock provider, define expectations
- Generate contract: Pact records the interaction as a JSON file
- Share contract: Contract file pushed to central repository
- Provider test: Provider tests against the contract; verifies provider actually implements it
- Deploy safely: If contract tests pass, deployment is safe
Practical Example
- JavaScript (Pact)
- Python (Pact)
- Go (Pact)
// Consumer test: Service A expects /user/{id}
const { Pact } = require('@pact-foundation/pact');
const { UserClient } = require('../src/userClient');
describe('User Service Consumer', () => {
const provider = new Pact({ consumer: 'ServiceA', provider: 'UserService' });
beforeAll(() => provider.setup());
afterAll(() => provider.finalize());
it('should fetch user by ID', async () => {
// Define what ServiceA expects from UserService
await provider.addInteraction({
state: 'user 123 exists',
uponReceiving: 'a request for user 123',
withRequest: {
method: 'GET',
path: '/users/123',
headers: { Authorization: 'Bearer token' }
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: 123,
name: 'Alice',
email: 'alice@example.com'
}
}
});
// Call the service (mock provider responds)
const client = new UserClient('http://localhost:8080');
const user = await client.getUser(123);
// Verify the response matches expectations
expect(user.id).toBe(123);
expect(user.name).toBe('Alice');
expect(user.email).toBe('alice@example.com');
});
it('should handle user not found', async () => {
await provider.addInteraction({
state: 'user 999 does not exist',
uponReceiving: 'a request for non-existent user',
withRequest: {
method: 'GET',
path: '/users/999'
},
willRespondWith: {
status: 404,
body: { error: 'User not found' }
}
});
const client = new UserClient('http://localhost:8080');
await expect(client.getUser(999)).rejects.toThrow('Not found');
});
});
# Consumer test: Service A expects /user/{id}
import pytest
from pact import Consumer, Provider
import requests
pact = Consumer('ServiceA').has_state(
'user 123 exists'
).upon_receiving(
'a request for user 123'
).with_request(
'GET',
'/users/123',
headers={'Authorization': 'Bearer token'}
).will_respond_with(
200,
body={
'id': 123,
'name': 'Alice',
'email': 'alice@example.com'
}
)
def test_fetch_user():
with pact:
response = requests.get('http://localhost:8000/users/123')
assert response.status_code == 200
data = response.json()
assert data['id'] == 123
assert data['name'] == 'Alice'
assert data['email'] == 'alice@example.com'
# Provider test: UserService implements the contract
def test_user_service_honors_contract(mock_db):
"""Verify provider actually implements the contract"""
from app import app
from pact import Consumer, Provider
# Load the contract generated by consumer
contract = Consumer('ServiceA').has_state(
'user 123 exists'
).upon_receiving(
'a request for user 123'
).with_request(
'GET', '/users/123'
).will_respond_with(200)
# Test that our provider actually implements it
client = app.test_client()
response = client.get('/users/123')
assert response.status_code == 200
data = response.get_json()
assert 'id' in data
assert 'name' in data
assert 'email' in data
// Consumer test: Service A expects /user/{id}
package main
import (
"fmt"
"net/http"
"testing"
"github.com/pact-foundation/pact-go/v2/consumer"
"github.com/pact-foundation/pact-go/v2/matchers"
"github.com/stretchr/testify/assert"
)
func TestUserServiceConsumerContract(t *testing.T) {
mockProvider, err := consumer.NewV4MockProvider()
assert.NoError(t, err)
mockProvider.
AddInteraction().
Given("user 123 exists").
UponReceiving("a request for user 123").
WithRequest("GET", "/users/123").
WithHeaders(map[string]string{"Authorization": "Bearer token"}).
WillRespondWith(200).
WithBody(map[string]interface{}{
"id": matchers.Int(123),
"name": matchers.String("Alice"),
"email": matchers.String("alice@example.com"),
})
err = mockProvider.ExecuteTest(t, func(mockURL string) error {
// Call the service with the mock provider URL
resp, err := http.Get(fmt.Sprintf("%s/users/123", mockURL))
if err != nil {
return err
}
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
return nil
})
assert.NoError(t, err)
}
// Provider test: Verify UserService implements contract
func TestUserServiceProvider(t *testing.T) {
// Load contract files from consumer
opts := &verifier.VerifyRequest{
ProviderBaseURL: "http://localhost:8080",
PactFiles: []string{"./pacts/ServiceA-UserService.json"},
ProviderVersion: "1.0.0",
}
// Verify the provider satisfies all contracts
err := verifier.VerifyProvider(t, opts)
assert.NoError(t, err)
}
When to Use / When Not to Use
- You have multiple services with API dependencies
- Services are owned by different teams
- You need fast feedback before integration testing
- API changes are frequent; contracts protect against breaking changes
- You want to prevent tight coupling between services
- You have a monolith (single service); unit/integration tests sufficient
- Services are tightly integrated (e.g., single team; easier to test integration)
- API contracts are stable and rarely change
- Full end-to-end testing is necessary (contracts test single API, not full workflows)
- Consumer-driven design would be overengineering
Patterns and Pitfalls
Contract Testing Best Practices and Anti-Patterns
Design Review Checklist
- Consumer defines API contract (what data is needed)
- Contract includes request/response structure and headers
- Contract specifies pre-conditions/state (e.g., 'user exists')
- Consumer test generates contract file via Pact
- Provider test verifies the provider implements the contract
- Both consumer and provider tests run in CI/CD
- Contracts are stored in version control or Pact broker
- Contract matches actual API behavior (no over-mocking)
- Breaking changes to API cause contract test failures
- Error scenarios tested (404, 5xx, validation errors)
- Contract includes realistic response data (not just {id: 1})
- Contracts are shared between consumer and provider teams
- Contract versions tracked alongside service versions
- Deployment gates on contract test failures
- Regular audits to ensure contracts match actual APIs
Self-Check Questions
-
Q: What's the difference between contract testing and integration testing? A: Contract testing validates API contract (request/response) with a mock provider (fast). Integration testing uses a real provider (slow, comprehensive).
-
Q: Why is it called 'consumer-driven' contracts? A: Because the consumer (service that uses the API) defines the contract first. The provider then implements to match.
-
Q: What does Pact do? A: Pact records interactions between consumer and mock provider as a contract file (JSON). Provider tests verify the provider actually implements the contract.
-
Q: How do you handle contract breaks (incompatible changes)? A: Consumer and provider must agree on new contract. Both teams update tests. Deployment gates on contract test failures force the conversation.
-
Q: Should you use contracts instead of integration testing? A: No. Use both. Contracts for fast feedback on API compatibility. Integration tests for confidence in full workflows.
Next Steps
- Identify service dependencies — Map which services call which APIs
- Start with critical APIs — Contract test the most frequently used APIs first
- Choose a CDC tool — Pact (polyglot), Spring Cloud Contract (.NET/Java)
- Write consumer tests — Define what the consumer expects from the API
- Generate contracts — Pact generates contract files
- Write provider tests — Verify provider implements the contract
- Share contracts — Central repository or Pact broker
- Automate verification — Run consumer and provider tests in CI/CD
- Monitor contract violations — Alert when contracts fail; prioritize fixes