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.

Easy Python 3.9+ Aug 9, 2026 System design patterns 15 views 0 copies

Python code

30 lines
Python 3.9+
from 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

stdout
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

  1. Use pytest-mock's mocker fixture for cleaner mock lifecycle management
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from System design patterns

Related tutorials and quizzes for this topic.