How to mock an external service in Python with an anti-corruption facade
This code implements an anti-corruption facade that mocks an external API, allowing client code to interact with a simulated service while keeping the same interface.
Python code
34 linesclass AntiCorruptionFacade:
"""Mocks a real API while keeping the same interface."""
def __init__(self, data_store):
self._data_store = data_store
self._calls = []
def get_user(self, user_id):
self._calls.append(f"get_user({user_id})")
return self._data_store.get(user_id, {}).copy()
def update_user(self, user_id, data):
self._calls.append(f"update_user({user_id})")
self._data_store[user_id] = data
return True
def get_calls(self):
return self._calls.copy()
if __name__ == "__main__":
# Simulate the external service's data
external_store = {
1: {"name": "Alice", "email": "alice@example.com"},
2: {"name": "Bob", "email": "bob@example.com"}
}
# Client code uses the facade as if it's the real service
facade = AntiCorruptionFacade(external_store)
print("Initial user 1:", facade.get_user(1))
print("Updating user 2:", facade.update_user(2, {"name": "Robert", "email": "robert@example.com"}))
print("Updated user 2:", facade.get_user(2))
print("Missing user 99:", facade.get_user(99))
print("Call log:", facade.get_calls())
Output
Initial user 1: {'name': 'Alice', 'email': 'alice@example.com'}
Updating user 2: True
Updated user 2: {'name': 'Robert', 'email': 'robert@example.com'}
Missing user 99: {}
Call log: ['get_user(1)', 'update_user(2)', 'get_user(2)', 'get_user(99)']
How it works
The AntiCorruptionFacade wraps a simple dictionary as a stand-in for an external API, exposing methods that return dicts just like a real client would. It records every call in _calls, enabling assertions in tests. The get_user method uses .get(user_id, {}) to safely handle missing keys, returning an empty dict instead of raising a KeyError. update_user writes directly to the store and returns True to mimic a success response. This pattern isolates client code from the actual external service, making it easy to swap mocks for real implementations later.
Common mistakes
- Not copying the returned dict, allowing callers to mutate internal state.
- Forgetting to track calls, making it hard to verify interactions.
- Raising exceptions for missing keys instead of returning defaults.
Variations
- Use a `Mock` from `unittest.mock` to dynamically record calls and set return values.
- Return a custom response object instead of a plain dict if the real API uses one.
Real-world use cases
- Testing microservice integrations without hitting the real third-party API.
- Simulating slow or flaky services in local development to ensure graceful degradation.
- Contract testing where the facade must match the external service's interface exactly.
Sponsored
More from Microservices patterns
- BFF aggregation pattern: combine multiple service responses in Python easy
- Backward Compatible Schema Evolution in Python medium
- Bulkhead Thread Pool per Service Mock in Python medium
- CQRS with Separate Read and Write Repositories in Python medium
- Cache-Aside Pattern in Python: Per-Service Mock easy
- Consumer Driven Contract Pact Mock in Python medium
Keep learning
Related tutorials and quizzes for this topic.