Mocking Strategies - InjectQ Documentation
Author: InjectQ Contributors
Description: Modern Python dependency injection library

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?