Skip to content

Mocking Strategies

Mocking strategies define different approaches to replacing real dependencies with test doubles during testing.

🎯 Mock Categories

Dummy Objects

Dummy objects are passed around but never actually used. They are typically used to satisfy parameter requirements.

class DummyEmailService:
    """Dummy implementation that does nothing."""
    def send_welcome_email(self, email: str) -> None:
        pass

    def send_password_reset(self, email: str, token: str) -> None:
        pass

def test_user_creation_with_dummy(container):
    """Test using dummy objects for unused dependencies."""
    # Bind dummy for dependency we don't care about
    container.bind(IEmailService, DummyEmailService())

    # Focus test on user creation logic
    user_service = container.get(IUserService)
    user = user_service.create_user("john@example.com", "password")

    # Only verify user creation
    assert user.email == "john@example.com"
    assert user.is_active is True

Stubs

Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.

class StubUserRepository:
    """Stub that returns predefined data."""
    def __init__(self):
        self.users = {
            1: User(id=1, email="existing@example.com", is_active=True)
        }

    def get_user(self, user_id: int) -> Optional[User]:
        return self.users.get(user_id)

    def save_user(self, user: User) -> User:
        # Always succeed for stub
        user.id = len(self.users) + 1
        self.users[user.id] = user
        return user

    def get_user_by_email(self, email: str) -> Optional[User]:
        return next((u for u in self.users.values() if u.email == email), None)

def test_email_uniqueness_check(container):
    """Test email uniqueness validation using stub."""
    stub_repo = StubUserRepository()
    container.bind(IUserRepository, stub_repo)

    user_service = container.get(IUserService)

    # Test with existing email
    with pytest.raises(EmailAlreadyExistsError):
        user_service.create_user("existing@example.com", "password")

    # Test with new email
    user = user_service.create_user("new@example.com", "password")
    assert user.email == "new@example.com"

Mocks

Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive.

from injectq.testing import Mock

class MockEmailService(Mock[IEmailService]):
    def __init__(self):
        super().__init__()
        self.sent_emails = []

    def send_welcome_email(self, email: str) -> None:
        self.record_call("send_welcome_email", email)
        self.sent_emails.append({
            "type": "welcome",
            "email": email,
            "timestamp": time.time()
        })

    def send_password_reset(self, email: str, token: str) -> None:
        self.record_call("send_password_reset", email, token)
        self.sent_emails.append({
            "type": "password_reset",
            "email": email,
            "token": token,
            "timestamp": time.time()
        })

def test_registration_sends_welcome_email(container):
    """Test that registration sends welcome email."""
    mock_email = MockEmailService()
    container.bind(IEmailService, mock_email)

    user_service = container.get(IUserService)
    user = user_service.register_user("john@example.com", "password")

    # Verify the behavior we care about
    assert mock_email.call_count("send_welcome_email") == 1
    assert mock_email.was_called_with("send_welcome_email", "john@example.com")
    assert user.email == "john@example.com"

Spies

Spies are stubs that also record some information based on how they were called.

class SpyUserRepository(Mock[IUserRepository]):
    """Spy that records calls while delegating to real implementation."""
    def __init__(self, real_repo: IUserRepository):
        super().__init__()
        self.real_repo = real_repo
        self.created_users = []
        self.queried_users = []

    def save_user(self, user: User) -> User:
        self.record_call("save_user", user)

        # Delegate to real implementation
        saved_user = self.real_repo.save_user(user)
        self.created_users.append(saved_user)

        return saved_user

    def get_user(self, user_id: int) -> Optional[User]:
        self.record_call("get_user", user_id)

        # Delegate to real implementation
        user = self.real_repo.get_user(user_id)
        if user:
            self.queried_users.append(user)

        return user

def test_user_operations_with_spy(container, test_db):
    """Test user operations using spy pattern."""
    real_repo = UserRepository(test_db)
    spy_repo = SpyUserRepository(real_repo)
    container.bind(IUserRepository, spy_repo)

    user_service = container.get(IUserService)

    # Create user
    user = user_service.create_user("john@example.com", "password")

    # Retrieve user
    retrieved_user = user_service.get_user(user.id)

    # Verify spy recorded interactions
    assert spy_repo.call_count("save_user") == 1
    assert spy_repo.call_count("get_user") == 1
    assert len(spy_repo.created_users) == 1
    assert len(spy_repo.queried_users) == 1
    assert spy_repo.created_users[0].id == user.id

Fakes

Fakes are working implementations with simplified functionality, not suitable for production use.

class FakeDatabase:
    """Fake database using in-memory storage."""
    def __init__(self):
        self.users = {}
        self.orders = {}
        self._next_user_id = 1
        self._next_order_id = 1

    def save_user(self, user: User) -> User:
        if user.id is None:
            user.id = self._next_user_id
            self._next_user_id += 1
        self.users[user.id] = user
        return user

    def get_user(self, user_id: int) -> Optional[User]:
        return self.users.get(user_id)

    def get_user_by_email(self, email: str) -> Optional[User]:
        return next((u for u in self.users.values() if u.email == email), None)

    def save_order(self, order: Order) -> Order:
        if order.id is None:
            order.id = self._next_order_id
            self._next_order_id += 1
        self.orders[order.id] = order
        return order

    def get_order(self, order_id: int) -> Optional[Order]:
        return self.orders.get(order_id)

    def clear(self):
        """Clear all data for test isolation."""
        self.users.clear()
        self.orders.clear()
        self._next_user_id = 1
        self._next_order_id = 1

def test_complete_workflow_with_fake(container):
    """Test complete workflow using fake database."""
    fake_db = FakeDatabase()
    container.bind(IDatabase, fake_db)

    # Test complete user journey
    user_service = container.get(IUserService)
    order_service = container.get(IOrderService)

    # Create user
    user = user_service.create_user("john@example.com", "password")
    assert user.id == 1

    # Create order
    order = order_service.create_order(user.id, ["item1", "item2"])
    assert order.id == 1
    assert order.user_id == user.id

    # Verify data persistence
    saved_user = fake_db.get_user(user.id)
    saved_order = fake_db.get_order(order.id)
    assert saved_user.email == "john@example.com"
    assert saved_order.user_id == user.id

🎨 Choosing the Right Strategy

When to Use Each Type

Use Dummies When:

  • You need to satisfy constructor parameters
  • The dependency won't be used in the test
  • You want to keep the test focused
def test_business_logic_only(container):
    """Test focuses only on business logic."""
    # Dummy for unused dependency
    container.bind(IExternalAPI, DummyExternalAPI())

    calculator = container.get(PriceCalculator)
    result = calculator.calculate(items)

    assert result.total == expected_total

Use Stubs When:

  • You need predictable responses
  • You want to test specific scenarios
  • The test doesn't care about side effects
def test_validation_with_stub(container):
    """Test validation with predictable data."""
    stub_repo = StubUserRepository()
    container.bind(IUserRepository, stub_repo)

    validator = container.get(EmailValidator)

    # Test existing email
    assert not validator.is_unique("existing@example.com")

    # Test new email
    assert validator.is_unique("new@example.com")

Use Mocks When:

  • You need to verify interactions
  • You want to ensure certain methods are called
  • You need to check call arguments
def test_notification_sent_on_registration(container):
    """Test that notification is sent during registration."""
    mock_email = MockEmailService()
    container.bind(IEmailService, mock_email)

    user_service = container.get(IUserService)
    user = user_service.register_user("john@example.com", "password")

    # Verify notification was sent
    assert mock_email.call_count("send_welcome_email") == 1
    assert mock_email.was_called_with("send_welcome_email", "john@example.com")

Use Spies When:

  • You want to verify interactions with real implementations
  • You need both real behavior and call verification
  • You're testing integration between components
def test_service_integration_with_spy(container, real_db):
    """Test service integration with spy verification."""
    real_repo = UserRepository(real_db)
    spy_repo = SpyUserRepository(real_repo)
    container.bind(IUserRepository, spy_repo)

    user_service = container.get(IUserService)
    user = user_service.create_user("john@example.com", "password")

    # Verify real database operation occurred
    assert user.id is not None

    # Verify spy recorded the interaction
    assert spy_repo.call_count("save_user") == 1

Use Fakes When:

  • You need realistic behavior for integration tests
  • You want to test against a real interface
  • Performance is important for test execution
def test_workflow_integration(container):
    """Test complete workflow with fake implementations."""
    container.bind(IDatabase, FakeDatabase())
    container.bind(ICache, FakeCache())

    # Test complete user journey
    workflow = container.get(UserRegistrationWorkflow)
    result = workflow.register_and_setup_user("john@example.com", "password")

    assert result.success is True
    assert result.user.email == "john@example.com"
    assert result.setup_complete is True

🔧 Advanced Mocking Patterns

Partial Mocks

class PartialMockEmailService(Mock[IEmailService]):
    """Mock that delegates some calls to real implementation."""
    def __init__(self, real_service: IEmailService):
        super().__init__()
        self.real_service = real_service

    def send_welcome_email(self, email: str) -> None:
        # Use real implementation for this method
        self.real_service.send_welcome_email(email)

    def send_password_reset(self, email: str, token: str) -> None:
        # Mock this method
        self.record_call("send_password_reset", email, token)

def test_partial_mock_usage(container):
    """Test using partial mock for selective mocking."""
    real_email = RealEmailService()
    partial_mock = PartialMockEmailService(real_email)
    container.bind(IEmailService, partial_mock)

    user_service = container.get(IUserService)

    # Welcome email uses real service
    user_service.register_user("john@example.com", "password")

    # Password reset uses mock
    user_service.reset_password("john@example.com")

    # Verify only password reset was mocked
    assert partial_mock.call_count("send_password_reset") == 1
    assert partial_mock.call_count("send_welcome_email") == 0

Chain of Mocks

class MockPaymentService(Mock[IPaymentService]):
    def __init__(self):
        super().__init__()
        self.authorized_payments = set()

    def authorize_payment(self, amount: float, card: str) -> str:
        self.record_call("authorize_payment", amount, card)

        # Generate mock authorization code
        auth_code = f"auth_{len(self.authorized_payments)}"
        self.authorized_payments.add(auth_code)

        return auth_code

    def capture_payment(self, auth_code: str, amount: float) -> bool:
        self.record_call("capture_payment", auth_code, amount)

        if auth_code in self.authorized_payments:
            self.authorized_payments.remove(auth_code)
            return True
        return False

def test_payment_workflow(container):
    """Test payment workflow with chained operations."""
    mock_payment = MockPaymentService()
    container.bind(IPaymentService, mock_payment)

    payment_service = container.get(IPaymentService)

    # Authorize payment
    auth_code = payment_service.authorize_payment(100.0, "4111111111111111")
    assert auth_code.startswith("auth_")

    # Capture payment
    success = payment_service.capture_payment(auth_code, 100.0)
    assert success is True

    # Verify call chain
    assert mock_payment.call_count("authorize_payment") == 1
    assert mock_payment.call_count("capture_payment") == 1

Mock Factories

class MockFactory:
    """Factory for creating configured mocks."""
    @staticmethod
    def create_email_service(fail_on_send: bool = False) -> MockEmailService:
        mock = MockEmailService()
        if fail_on_send:
            mock.configure_exception("send_welcome_email", SMTPError("Connection failed"))
        return mock

    @staticmethod
    def create_user_repository(users: List[User] = None) -> StubUserRepository:
        stub = StubUserRepository()
        if users:
            for user in users:
                stub.users[user.id] = user
        return stub

    @staticmethod
    def create_database(initial_data: dict = None) -> FakeDatabase:
        fake = FakeDatabase()
        if initial_data:
            fake.users.update(initial_data.get("users", {}))
            fake.orders.update(initial_data.get("orders", {}))
        return fake

def test_with_factory_mocks(container):
    """Test using mock factory for consistent setup."""
    # Create configured mocks
    email_mock = MockFactory.create_email_service()
    user_repo = MockFactory.create_user_repository([
        User(id=1, email="existing@example.com", is_active=True)
    ])

    container.bind(IEmailService, email_mock)
    container.bind(IUserRepository, user_repo)

    user_service = container.get(IUserService)

    # Test with pre-configured state
    with pytest.raises(EmailAlreadyExistsError):
        user_service.create_user("existing@example.com", "password")

🚨 Common Mocking Mistakes

❌ Over-Mocking

# Bad: Mocking everything
def test_with_too_many_mocks(container):
    container.bind_mock(IUserService)
    container.bind_mock(IEmailService)
    container.bind_mock(IValidator)
    container.bind_mock(IPasswordHasher)
    container.bind_mock(ILogger)
    # Test becomes meaningless

# Good: Mock only external dependencies
def test_focused_test(container):
    # Mock external services only
    container.bind_mock(IEmailService)  # External API
    container.bind_mock(IPaymentService)  # External payment

    # Use real implementations for business logic
    # Test focuses on actual behavior

❌ Mocking Internal Logic

# Bad: Mocking internal business logic
def test_internal_logic_mock(container):
    mock_calculator = MockPriceCalculator()
    mock_calculator.configure_return("calculate_tax", 10.0)
    container.bind(IPriceCalculator, mock_calculator)

    service = container.get(OrderService)
    total = service.calculate_total(order)

    # Test is testing the mock, not real logic
    assert total == 110.0  # Based on mock return

# Good: Test real business logic
def test_real_business_logic(container):
    # Use real calculator
    container.bind(IPriceCalculator, PriceCalculator())

    # Mock only external dependencies
    container.bind_mock(ITaxService)  # External tax API

    service = container.get(OrderService)
    total = service.calculate_total(order)

    # Test is testing real calculation logic
    assert total == expected_total

❌ Ignoring Mock Configuration

# Bad: Mock not configured for test scenario
def test_unconfigured_mock(container):
    mock_repo = MockUserRepository()
    # Forgot to configure for "user not found" scenario
    container.bind(IUserRepository, mock_repo)

    service = container.get(UserService)

    # Test expects user not found, but mock returns None by default
    user = service.get_user(999)
    assert user is None  # This might fail if mock not configured

# Good: Properly configure mock for scenario
def test_configured_mock(container):
    mock_repo = MockUserRepository()
    mock_repo.configure_return("get_user", None)  # Explicitly configure
    container.bind(IUserRepository, mock_repo)

    service = container.get(UserService)
    user = service.get_user(999)
    assert user is None

📊 Mocking Best Practices

1. Mock External Dependencies Only

# ✅ Good: Mock external services
container.bind_mock(IEmailService)      # External API
container.bind_mock(IPaymentGateway)    # External payment
container.bind_mock(ISMSService)        # External SMS

# ✅ Use real implementations for business logic
container.bind(IPriceCalculator, PriceCalculator())  # Real business logic
container.bind(IOrderValidator, OrderValidator())    # Real validation

2. Use Appropriate Mock Types

# ✅ Use stubs for predictable data
container.bind(IUserRepository, StubUserRepository())

# ✅ Use mocks for interaction verification
container.bind(IEmailService, MockEmailService())

# ✅ Use fakes for realistic behavior
container.bind(IDatabase, FakeDatabase())

# ✅ Use spies for integration testing
container.bind(IUserRepository, SpyUserRepository(real_repo))

3. Configure Mocks Explicitly

# ✅ Explicit mock configuration
mock_email = MockEmailService()
mock_email.configure_return("send_welcome_email", None)
mock_email.configure_exception("send_password_reset", SMTPError())
container.bind(IEmailService, mock_email)

# ✅ Clear mock configuration between tests
@pytest.fixture
def configured_mock():
    mock = MockEmailService()
    mock.configure_return("send_welcome_email", None)
    return mock

4. Verify Important Interactions

# ✅ Verify critical interactions
def test_payment_processing(container):
    mock_payment = MockPaymentService()
    container.bind(IPaymentService, mock_payment)

    order_service = container.get(IOrderService)
    order_service.process_payment(order)

    # Verify payment was processed
    assert mock_payment.call_count("charge_card") == 1
    assert mock_payment.was_called_with("charge_card", order.total, order.card_token)

# ✅ Don't verify unimportant details
def test_user_creation(container):
    mock_email = MockEmailService()
    container.bind(IEmailService, mock_email)

    user_service = container.get(IUserService)
    user = user_service.create_user("john@example.com", "password")

    # Verify result, not email sending details
    assert user.email == "john@example.com"
    # Don't check email template or exact timing

5. Keep Mocks Simple

# ✅ Simple, focused mock
class SimpleMockEmailService(Mock[IEmailService]):
    def send_welcome_email(self, email: str) -> None:
        self.record_call("send_welcome_email", email)

# ❌ Complex mock with too much logic
class ComplexMockEmailService(Mock[IEmailService]):
    def __init__(self):
        self.templates = {}
        self.queue = []
        self.retry_count = 3
        self.timeout = 30
        # ... lots of configuration

    def send_welcome_email(self, email: str) -> None:
        # Complex logic that might have bugs
        if self.should_retry:
            for i in range(self.retry_count):
                try:
                    self._send_with_template(email, "welcome")
                    break
                except Exception:
                    if i == self.retry_count - 1:
                        raise
        # ... more complex logic

🎯 Summary

Mocking strategies provide different ways to replace dependencies:

  • Dummies - Satisfy parameters, never used
  • Stubs - Provide canned responses
  • Mocks - Verify interactions and expectations
  • Spies - Record calls while using real implementations
  • Fakes - Working implementations with simplified behavior

Key principles: - Choose the right strategy for each test scenario - Mock external dependencies, use real business logic - Configure mocks explicitly for test scenarios - Verify important interactions, not implementation details - Keep mocks simple and focused - Use factories for consistent mock creation

Best practices: - Use dummies for unused dependencies - Use stubs for predictable test data - Use mocks for interaction verification - Use spies for integration testing - Use fakes for realistic behavior - Avoid over-mocking and complex mock logic

Ready to explore override patterns?