How to Mock Hexagonal Architecture Ports and Adapters in Python
Mock an email adapter in a hexagonal architecture with unittest.mock to test business logic in isolation.
Python code
30 linesfrom unittest.mock import Mock
class EmailService:
def send(self, recipient, message):
raise NotImplementedError
class OrderProcessor:
def __init__(self, email_service):
self.email_service = email_service
def process_order(self, order_id, customer_email):
# Business logic
confirmation = f"Order {order_id} confirmed"
self.email_service.send(customer_email, confirmation)
return confirmation
def main():
# Create mock adapter
mock_email = Mock()
mock_email.send.return_value = True
processor = OrderProcessor(mock_email)
result = processor.process_order("ORD-123", "customer@example.com")
print(f"Result: {result}")
print(f"Email sent: {mock_email.send.called}")
print(f"Email called with: {mock_email.send.call_args}")
if __name__ == "__main__":
main()
Output
Result: Order ORD-123 confirmed
Email sent: True
Email called with: call('customer@example.com', 'Order ORD-123 confirmed')
How it works
The EmailService class defines the port as an abstract contract with a send method. The Mock() object represents an adapter that injects at runtime, decoupling the OrderProcessor from external dependencies. Setting return_value = True controls what the mock returns so the test focuses on business logic. Checking called and call_args verifies interaction without hitting a real SMTP server. This pattern lets you swap adapters (e.g., SendGrid, SMTP) without changing the core domain.
Common mistakes
- Forgetting to inject the mock — instantiating a real email service raises NotImplementedError
- Not setting return_value when the business logic depends on the adapter's result
- Over-mocking and losing coverage of the real adapter integration
Variations
- Use pytest-mock's mocker fixture for cleaner mock lifecycle management
- Create a custom fake class instead of Mock for stricter type checking
Real-world use cases
- Unit testing order or payment services without sending real notifications in CI.
- Simulating slow or flaky third-party APIs to verify timeout and retry logic.
- Isolating service tests so a downstream outage never blocks your test suite.
Sponsored
More from System design patterns
- Build a BFF (Backend for Frontend) Mock Aggregator in Python medium
- Builder pattern for mocking complex objects in Python easy
- Circuit Breaker Pattern in Python: Closed, Open, and Half-Open States medium
- Create a Data Helper Class in Python easy
- Domain Driven Design Aggregate Root Example in Python medium
- Facade Pattern in Python with Mock Simplification medium
Keep learning
Related tutorials and quizzes for this topic.