Testing Microservices Integration — Consumer-Driven Contract Tests Guide
In this tutorial, you'll learn about Testing Microservices Integration. We cover key concepts, practical examples, and best practices.
Microservices integration testing is the most challenging layer of the test pyramid — services depend on other services, each with its own deployment cycle, data store, and team ownership. In this guide, you will learn how to test microservice integrations using consumer-driven contracts with Pact, virtualize dependent services, and build integration test suites that run in CI without fragile end-to-end environments. The DodaTech backend platform uses Pact contracts between all 15 microservices, enabling independent deployment with zero integration surprises.
Learning Path
flowchart LR A[Microservices Architecture] --> B[Integration Testing] B --> C[Contract Testing
You are here] C --> D[Consumer-Driven Contracts] D --> E[Independent Deployment] style C fill:#f90,color:#fff
The Integration Testing Challenge
| Approach | Speed | Isolation | Confidence |
|---|---|---|---|
| E2E testing | Slow | None | High |
| Contract testing | Fast | Full | Medium-High |
| Mock-based | Fast | Full | Low |
| Service virtualization | Medium | Full | Medium |
Consumer-Driven Contract with Pact
The consumer defines its expectations first, then the provider verifies them.
Consumer Test (Python)
import atheris
import logging
# Consumer test defines the expected interaction
from pact import Consumer, Provider
pact = Consumer("WebFrontend").has_pact_with(Provider("UserService"))
pact.start_service()
pact.upon_receiving("a request for user details") \
.with_request("get", "/users/1") \
.will_respond_with(200, body={
"id": 1,
"name": "Alice",
"email": "alice@example.com"
})
with pact:
result = pact.setup()
# In real code, make HTTP call to mock server
print("Consumer contract defined for UserService")
pact.stop_service()
Provider Verification (JavaScript)
const { Verifier } = require('@pact-foundation/pact');
describe('UserService provider verification', () => {
it('verifies all consumer contracts', async () => {
const verifier = new Verifier({
provider: 'UserService',
providerBaseUrl: 'http://localhost:3001',
pactBrokerUrl: 'https://pact-broker.example.com',
stateHandlers: {
'a user exists with ID 1': async () => {
// Set up database state
await setupUser({ id: 1, name: 'Alice', email: 'alice@example.com' });
},
},
});
return verifier.verifyProvider();
});
});
Expected output:
Verifying contract for WebFrontend
✓ returns user details
✓ returns 404 for non-existent user
2 interactions verified, 0 failures
Service Virtualization with WireMock
When the dependent service is not available, virtualize it:
from wiremock.client import *
def setup_mock_user_service():
MappingBuilder(
MappingMatchers.url_path_equal_to("/users/1"),
Methods.GET
).will_return(
ResponseDefinitionBuilder()
.with_status(200)
.with_header("Content-Type", "application/json")
.with_body('{"id": 1, "name": "Alice", "email": "alice@test.com"}')
)
Mappings.create_mapping(
MappingBuilder(
MappingMatchers.url_path_matching("/users/\\d+"),
Methods.GET
)
)
def test_with_virtual_service():
response = requests.get("http://localhost:8080/users/1")
assert response.status_code == 200
assert response.json()["name"] == "Alice"
print("Virtual service test passed")
Testing Asynchronous Integration (Message Queues)
Microservices often communicate via message queues:
import json, time
class MessageQueueTestClient:
def __init__(self):
self.messages = []
def publish(self, topic, message):
self.messages.append({"topic": topic, "message": message, "time": time.time()})
return True
def consume(self, topic, timeout=5):
matching = [m for m in self.messages if m["topic"] == topic]
return matching[-1] if matching else None
def test_order_processing():
queue = MessageQueueTestClient()
order = {"order_id": "ORD-001", "user_id": 1, "total": 99.99}
queue.publish("orders", order)
time.sleep(0.1)
received = queue.consume("orders")
assert received is not None
assert received["message"]["order_id"] == "ORD-001"
assert received["message"]["total"] == 99.99
print("Async message test passed")
test_order_processing()
Integration Test in CI
name: Microservice Integration Tests
on: [pull_request]
jobs:
contract-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Start service
run: docker compose up -d user-service
- name: Run provider verification
run: npm run pact:verify
- name: Publish results
if: always()
run: npm run pact:publish
Practice Questions
1. What is consumer-driven contract testing?
The consumer defines what it expects from the provider, and the provider verifies it can meet those expectations. This catches breaking changes before deployment.
2. How does contract testing differ from end-to-end testing?
Contract testing tests one service pair in isolation (fast, precise). E2E testing tests the full system (slow, broad). Contract tests catch integration issues earlier.
3. What is the Pact Broker and why is it important?
The Pact Broker stores and versions contracts, enabling consumers and providers to coordinate contract changes across deployment cycles.
4. How do you test asynchronous microservice communication?
Mock the message broker or use an in-memory queue for unit tests. Use a real broker (Kafka, RabbitMQ) in integration tests with Docker.
Challenge: Create contracts between three microservices: Order Service, Payment Service, and Notification Service. Define consumer tests for each pair, add provider verification, and build a CI pipeline that runs all verifications and publishes results.
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