Contract Testing — Consumer-Driven Contracts (2026)
In this tutorial, you'll learn about Contract Testing. We cover key concepts, practical examples, and best practices.
Contract testing verifies that two services (a consumer and a provider) can communicate correctly by capturing the interactions in a contract and verifying both sides independently.
What You'll Learn
You'll understand consumer-driven contracts, how the Pact framework works, the provider verification process, how contract testing ensures microservice compatibility, and how it differs from end-to-end testing.
Why Contract Testing Matters
In a microservice architecture, services depend on each other's APIs. When one service changes its response format, dependent services break. E2E tests catch this too late and are slow. Contract tests catch it at development time, per service, in isolation. At DodaTech, Doda Browser's backend microservices use Pact contracts to ensure the search service and user service remain compatible without full integration environments.
Contract Testing Learning Path
flowchart LR A[Integration Testing] --> B[Contract Testing] B --> C[Consumer-Driven Contracts] C --> D[Provider Verification] B --> E[API Testing Guide] style B fill:#f90,color:#fff
Consumer-Driven Contracts
In a consumer-driven contract, the consumer defines what it needs from the provider. The provider then verifies it can meet those needs.
Consumer (Frontend) ── defines contract ──> Pact Broker
Provider (API) ── verifies contract ──> Pact Broker
Pact Framework
Pact is the most popular contract testing framework, supporting multiple languages.
Consumer Test (Frontend/API Client)
// consumer/pactTest.js
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like } = MatchersV3;
const provider = new PactV3({
consumer: 'WebFrontend',
provider: 'UserService',
});
describe('User Service contract', () => {
it('returns user by ID', async () => {
// Define expected interaction
provider
.given('a user exists with ID 1')
.uponReceiving('a request for user 1')
.withRequest({
method: 'GET',
path: '/users/1',
})
.willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: like({
id: 1,
name: 'Alice',
email: 'alice@example.com',
}),
});
await provider.executeTest(async (mockServer) => {
// Make real HTTP call to mock server
const response = await fetch(`${mockServer.url}/users/1`);
const user = await response.json();
expect(response.status).toBe(200);
expect(user.name).toBeDefined();
});
});
});
Running this test:
- Starts a mock server
- Makes the real HTTP call
- Verifies the response matches the contract
- Publishes the contract to the Pact Broker
Provider Verification (API/Backend)
// provider/pactVerification.js
const { Verifier } = require('@pact-foundation/pact');
describe('User Service provider verification', () => {
it('verifies all consumer contracts', async () => {
const verifier = new Verifier({
provider: 'UserService',
providerBaseUrl: 'http://localhost:3001',
pactBrokerUrl: 'https://pact-broker.example.com',
});
return verifier.verifyProvider();
});
});
Expected output:
Verifying contract for WebFrontend
✓ returns user by ID
✓ returns 404 for non-existent user
2 interactions verified, 0 failures
Provider States
Provider states set up the provider in the right condition for each test.
// Provider states setup
provider.setupState((state) => {
if (state === 'a user exists with ID 1') {
// Seed database with user data
db.users.insert({ id: 1, name: 'Alice', email: 'alice@example.com' });
}
if (state === 'no user exists with ID 999') {
// Ensure user does not exist
db.users.delete({ id: 999 });
}
});
How Contract Testing Differs from E2E
| Aspect | Contract Testing | E2E Testing |
|---|---|---|
| Scope | Service pair | Full system |
| Speed | Milliseconds | Minutes |
| Isolation | Each service independently | All services together |
| Failure localization | Exact failing service | Unknown which service failed |
| Environment | CI (no full env needed) | Full staging environment |
| Maintenance | Low — contracts are focused | High — full system setup |
Pact Workflow
flowchart LR
A[Consumer writes test] --> B[Pact generates contract]
B --> C[Contract published to Broker]
C --> D[Provider fetches contract]
D --> E[Provider verifies against contract]
E --> F{Verification passes?}
F -->|Yes| G[Deploy safe]
F -->|No| H[Fix provider or update contract]
Best Practices
1. Start with Critical Integrations
Not every pair of services needs a contract. Focus on external-facing APIs and cross-team boundaries.
2. Keep Contracts Small
A contract should verify the interaction, not the business logic. Minimal fields, realistic examples.
3. Use Matchers for Flexible Matching
Pact matchers allow flexible matching (like type matching, regex) instead of exact values.
body: like({
id: integer(),
name: string(),
createdAt: timestamp(),
})
4. Version Contracts
Use the Pact Broker to version contracts. Old consumer versions should still work with updated providers.
5. Automate Provider Verification
Run provider verification in CI. Block deployment if verification fails.
6. Don't Test Everything with Contracts
Use contract tests for API compatibility. Use unit tests for business logic. Use E2E tests for user journeys.
Common Mistakes
1. Contracts Too Large
A contract with 50 fields is brittle. Only include fields the consumer actually uses.
2. No Provider States
Without states, verification is limited to existing data, which may not cover edge cases.
3. Testing Implementation Details
Contract tests verify HTTP interactions, not internal logic or database queries.
4. Skipping Provider Verification
A consumer contract without provider verification is just documentation, not testing.
5. Duplicating E2E Coverage
Don't contract-test what you E2E-test. Each has its purpose.
6. Not Handling Error Responses
Test 404, 400, and 500 responses, not just 200.
7. Ignoring Contract Drift
Contracts that aren't updated with API changes become stale and worthless.
Practice Questions
1. What is a consumer-driven contract? A contract where the consumer defines its expectations from the provider, and the provider verifies it can meet them.
2. How does contract testing differ from E2E testing? Contract testing tests one service pair in isolation (fast, precise). E2E tests the full system (slow, broad).
3. What are provider states in Pact? Preconditions that set up the provider with specific data before verification (e.g., "a user exists").
4. What is the Pact Broker? A repository for sharing and versioning contracts between consumers and providers.
5. Challenge: Write a contract for a payment API. Define a consumer test for a successful payment, an insufficient funds response, and an invalid card response.
Mini Project: Pact Contract for Product API
// consumer test
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const { like, eachLike } = MatchersV3;
const provider = new PactV3({
consumer: 'StoreFrontend',
provider: 'ProductService',
});
describe('ProductService contract', () => {
it('returns product list', async () => {
provider
.given('products exist')
.uponReceiving('a request for all products')
.withRequest({ method: 'GET', path: '/products' })
.willRespondWith({
status: 200,
body: eachLike({
id: 1,
name: 'Widget',
price: 9.99,
inStock: true,
}, { min: 1 }),
});
await provider.executeTest(async (mockServer) => {
const res = await fetch(`${mockServer.url}/products`);
const products = await res.json();
expect(products.length).toBeGreaterThan(0);
expect(products[0].name).toBeDefined();
});
});
it('returns 404 for unknown product', async () => {
provider
.given('no product exists with ID 999')
.uponReceiving('a request for non-existent product')
.withRequest({ method: 'GET', path: '/products/999' })
.willRespondWith({ status: 404 });
await provider.executeTest(async (mockServer) => {
const res = await fetch(`${mockServer.url}/products/999`);
expect(res.status).toBe(404);
});
});
});
FAQ
What's Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro