Skip to main content

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

Contract testing validates API contracts; integration testing validates end-to-end interactions
AspectContract TestingIntegration Testing
ProviderMockReal
SpeedFast (100s ms)Slow (seconds)
SetupDefine contractStart full system
ScopeAPI contract onlyFull interaction
CI/CD CostLowHigh
When to runEvery commitNightly or on release

Consumer-Driven Contracts (CDC)

CDC flips the responsibility model:

Traditional (Provider-Driven):

  1. Provider team designs API
  2. Consumer team uses API
  3. Breaking changes might break consumers

Consumer-Driven (CDC):

  1. Consumer team defines needs: "I need GET /user/:id returning id, name, email"
  2. Provider team implements that contract
  3. Provider knows exactly what consumers need; harder to break contracts accidentally

Pact and Contract Recording

Pact is a CDC framework:

  1. Consumer test: Mock provider, define expectations
  2. Generate contract: Pact records the interaction as a JSON file
  3. Share contract: Contract file pushed to central repository
  4. Provider test: Provider tests against the contract; verifies provider actually implements it
  5. Deploy safely: If contract tests pass, deployment is safe

Practical Example

// 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');
});
});

When to Use / When Not to Use

Use Contract Testing When:
  1. You have multiple services with API dependencies
  2. Services are owned by different teams
  3. You need fast feedback before integration testing
  4. API changes are frequent; contracts protect against breaking changes
  5. You want to prevent tight coupling between services
Avoid Contract Testing When:
  1. You have a monolith (single service); unit/integration tests sufficient
  2. Services are tightly integrated (e.g., single team; easier to test integration)
  3. API contracts are stable and rarely change
  4. Full end-to-end testing is necessary (contracts test single API, not full workflows)
  5. Consumer-driven design would be overengineering

Patterns and Pitfalls

Contract Testing Best Practices and Anti-Patterns

Start with consumer: Consumer defines what it needs; provider implements. Share contracts: Store contracts in a repository accessible to both teams. Version contracts: Contract versions should match service versions. Test provider thoroughly: Provider tests must verify every contract interaction. Automate contract verification: Run in CI/CD; fail deployment if contracts don't match. Use Pact broker: Share contracts between teams; manage contract lifecycle. Document context: Include state/pre-conditions in contract (e.g., 'user 123 exists'). Mock external services: Contracts should test the API boundary, not third-party dependencies.
Tight coupling to implementation: Testing internal state/database instead of API contract. Unclear contract states: Contract says 'user exists' but doesn't define how to set it up. No provider verification: Consumer tests pass but provider doesn't actually implement the contract. Contracts not in sync: Consumer and provider have different expectations. Over-mocking: Contract test so abstracted from reality it doesn't catch real issues. No version management: API changes without updating contracts. Ignoring contract violations: Contracts fail but deployment proceeds anyway. Contracts instead of integration tests: Some full integration testing is still necessary; contracts complement but don't replace it.

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

  1. Identify service dependencies — Map which services call which APIs
  2. Start with critical APIs — Contract test the most frequently used APIs first
  3. Choose a CDC tool — Pact (polyglot), Spring Cloud Contract (.NET/Java)
  4. Write consumer tests — Define what the consumer expects from the API
  5. Generate contracts — Pact generates contract files
  6. Write provider tests — Verify provider implements the contract
  7. Share contracts — Central repository or Pact broker
  8. Automate verification — Run consumer and provider tests in CI/CD
  9. Monitor contract violations — Alert when contracts fail; prioritize fixes

References

  1. Pact Foundation ↗️
  2. Consumer-Driven Contracts (Martin Fowler) ↗️
  3. Pact Documentation ↗️
  4. Spring Cloud Contract ↗️