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

Skip to content

# Testing Utilities API

::: injectq.testing

## Overview

The testing module provides comprehensive utilities for testing applications that use dependency injection, including mocking, test containers, and integration testing tools.

## Test Container

### Basic Test Container

    from injectq.testing import TestContainer
    from injectq import Container, inject

    # Create test container
    test_container = TestContainer()

    # Register test services
    test_container.register(UserRepository, MockUserRepository)
    test_container.register(EmailService, MockEmailService)

    # Use in tests
    @inject
    def test_user_service(user_service: UserService):
        # Test with mocked dependencies
        result = user_service.create_user("test@example.com")
        assert result.email == "test@example.com"

    # Run test with container
    test_container.run_test(test_user_service)

### Test Container Implementation

    from typing import Dict, Any, Optional, Type, TypeVar, Callable
    import inspect
    from contextlib import contextmanager

    T = TypeVar('T')

    class TestContainer:
        """Container optimized for testing scenarios."""

        def __init__(self, base_container: Optional[Container] = None):
            self.base_container = base_container
            self._test_registrations: Dict[Type, Any] = {}
            self._original_registrations: Dict[Type, Any] = {}
            self._active_mocks: Dict[Type, Any] = {}

        def register(self, service_type: Type[T], implementation: Any, scope: str = "transient") -> 'TestContainer':
            """Register a test service."""
            self._test_registrations[service_type] = {
                'implementation': implementation,
                'scope': scope
            }
            return self

        def register_mock(self, service_type: Type[T], mock_instance: Any = None) -> 'TestContainer':
            """Register a mock for a service type."""
            if mock_instance is None:
                # Create mock automatically
                try:
                    from unittest.mock import Mock, MagicMock

                    if inspect.isclass(service_type) and hasattr(service_type, '__abstractmethods__'):
                        # For abstract classes, use MagicMock
                        mock_instance = MagicMock(spec=service_type)
                    else:
                        mock_instance = Mock(spec=service_type)
                except ImportError:
                    raise ImportError("unittest.mock is required for automatic mock creation")

            self._active_mocks[service_type] = mock_instance
            return self.register(service_type, lambda: mock_instance, scope="singleton")

        def get_mock(self, service_type: Type[T]) -> Any:
            """Get the mock instance for a service type."""
            return self._active_mocks.get(service_type)

        def reset_mocks(self):
            """Reset all registered mocks."""
            for mock in self._active_mocks.values():
                if hasattr(mock, 'reset_mock'):
                    mock.reset_mock()

        @contextmanager
        def override_container(self):
            """Context manager to temporarily override the main container."""
            if self.base_container:
                # Store original registrations
                for service_type, registration in self._test_registrations.items():
                    if self.base_container._registry.is_registered(service_type):
                        self._original_registrations[service_type] = self.base_container._registry.get_binding(service_type)

                    # Override with test registration
                    self.base_container.register(
                        service_type,
                        registration['implementation'],
                        scope=registration['scope']
                    )

            try:
                yield self
            finally:
                if self.base_container:
                    # Restore original registrations
                    for service_type in self._test_registrations:
                        if service_type in self._original_registrations:
                            original = self._original_registrations[service_type]
                            self.base_container.register(
                                service_type,
                                original.implementation,
                                scope=original.scope.name
                            )
                        else:
                            # Remove test registration
                            if self.base_container._registry.is_registered(service_type):
                                self.base_container._registry.unregister(service_type)

                    self._original_registrations.clear()

        def run_test(self, test_func: Callable, *args, **kwargs):
            """Run a test function with the test container."""
            with self.override_container():
                if self.base_container:
                    return self.base_container.resolve(test_func, *args, **kwargs)
                else:
                    # Create temporary container for test
                    temp_container = Container()

                    for service_type, registration in self._test_registrations.items():
                        temp_container.register(
                            service_type,
                            registration['implementation'],
                            scope=registration['scope']
                        )

                    return temp_container.resolve(test_func, *args, **kwargs)

        def create_test_scope(self) -> 'TestScope':
            """Create a test scope for scoped services."""
            return TestScope(self)

    class TestScope:
        """Test scope for managing scoped service lifecycles."""

        def __init__(self, test_container: TestContainer):
            self.test_container = test_container
            self._scoped_instances: Dict[Type, Any] = {}

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_val, exc_tb):
            self.dispose()

        def dispose(self):
            """Dispose all scoped instances."""
            for instance in self._scoped_instances.values():
                if hasattr(instance, 'dispose'):
                    instance.dispose()
                elif hasattr(instance, '__exit__'):
                    instance.__exit__(None, None, None)

            self._scoped_instances.clear()

## Mock Factory

### Mock Creation

    from typing import Type, Dict, Any, List
    from unittest.mock import Mock, MagicMock, PropertyMock
    import inspect

    class MockFactory:
        """Factory for creating service mocks."""

        def __init__(self):
            self._mock_configurations: Dict[Type, Dict[str, Any]] = {}

        def create_mock(self, service_type: Type[T], **kwargs) -> Any:
            """Create a mock for a service type."""
            config = self._mock_configurations.get(service_type, {})
            config.update(kwargs)

            if inspect.isclass(service_type):
                if hasattr(service_type, '__abstractmethods__') and service_type.__abstractmethods__:
                    # Abstract class or interface
                    mock = MagicMock(spec=service_type, **config)
                else:
                    # Concrete class
                    mock = Mock(spec=service_type, **config)
            else:
                # Protocol or other type
                mock = Mock(spec=service_type, **config)

            self._configure_mock_behavior(mock, service_type, config)
            return mock

        def configure_mock(self, service_type: Type, **config):
            """Configure mock behavior for a service type."""
            self._mock_configurations[service_type] = config
            return self

        def _configure_mock_behavior(self, mock: Mock, service_type: Type, config: Dict[str, Any]):
            """Configure specific mock behaviors."""
            # Configure return values
            if 'return_values' in config:
                for method_name, return_value in config['return_values'].items():
                    getattr(mock, method_name).return_value = return_value

            # Configure side effects
            if 'side_effects' in config:
                for method_name, side_effect in config['side_effects'].items():
                    getattr(mock, method_name).side_effect = side_effect

            # Configure properties
            if 'properties' in config:
                for prop_name, prop_value in config['properties'].items():
                    prop_mock = PropertyMock(return_value=prop_value)
                    setattr(type(mock), prop_name, prop_mock)

    # Usage examples
    mock_factory = MockFactory()

    # Configure mock behavior
    mock_factory.configure_mock(
        UserRepository,
        return_values={
            'get_user': User(id=1, email="test@example.com"),
            'exists': True
        },
        side_effects={
            'delete_user': lambda user_id: None if user_id > 0 else ValueError("Invalid ID")
        }
    )

    # Create configured mock
    user_repo_mock = mock_factory.create_mock(UserRepository)

### Smart Mocks

    class SmartMock:
        """Mock that automatically handles common patterns."""

        def __init__(self, service_type: Type):
            self.service_type = service_type
            self._mock = self._create_smart_mock()

        def _create_smart_mock(self) -> Mock:
            """Create mock with intelligent defaults."""
            mock = Mock(spec=self.service_type)

            # Analyze service type for common patterns
            if hasattr(self.service_type, '__annotations__'):
                self._configure_property_mocks(mock)

            if hasattr(self.service_type, '__abstractmethods__'):
                self._configure_abstract_methods(mock)

            # Set up common return types
            self._configure_common_returns(mock)

            return mock

        def _configure_property_mocks(self, mock: Mock):
            """Configure property mocks based on type annotations."""
            annotations = getattr(self.service_type, '__annotations__', {})

            for name, annotation in annotations.items():
                if annotation == bool:
                    setattr(mock, name, True)
                elif annotation == int:
                    setattr(mock, name, 1)
                elif annotation == str:
                    setattr(mock, name, "test_value")
                elif annotation == list:
                    setattr(mock, name, [])
                elif annotation == dict:
                    setattr(mock, name, {})

        def _configure_abstract_methods(self, mock: Mock):
            """Configure abstract methods with sensible defaults."""
            abstract_methods = getattr(self.service_type, '__abstractmethods__', set())

            for method_name in abstract_methods:
                method = getattr(mock, method_name)

                # Analyze method signature for return type
                if hasattr(self.service_type, method_name):
                    original_method = getattr(self.service_type, method_name)
                    if hasattr(original_method, '__annotations__'):
                        return_annotation = original_method.__annotations__.get('return')
                        if return_annotation:
                            method.return_value = self._create_default_value(return_annotation)

        def _configure_common_returns(self, mock: Mock):
            """Configure common method return patterns."""
            # Methods that typically return self (fluent interface)
            fluent_methods = ['configure', 'setup', 'with_', 'add_', 'set_']

            for attr_name in dir(self.service_type):
                if any(attr_name.startswith(prefix) for prefix in fluent_methods):
                    if hasattr(mock, attr_name):
                        getattr(mock, attr_name).return_value = mock

        def _create_default_value(self, annotation: Type) -> Any:
            """Create default value for type annotation."""
            if annotation == bool:
                return True
            elif annotation == int:
                return 0
            elif annotation == str:
                return ""
            elif annotation == list:
                return []
            elif annotation == dict:
                return {}
            elif annotation == None:
                return None
            else:
                # For complex types, return a mock
                return Mock(spec=annotation)

        def __getattr__(self, name):
            """Delegate to underlying mock."""
            return getattr(self._mock, name)

## Test Utilities

### Assertion Helpers

    class DIAssertions:
        """Assertion helpers for dependency injection testing."""

        def __init__(self, container):
            self.container = container

        def assert_registered(self, service_type: Type):
            """Assert that a service is registered."""
            if not self.container._registry.is_registered(service_type):
                raise AssertionError(f"Service {service_type.__name__} is not registered")

        def assert_not_registered(self, service_type: Type):
            """Assert that a service is not registered."""
            if self.container._registry.is_registered(service_type):
                raise AssertionError(f"Service {service_type.__name__} is registered")

        def assert_singleton(self, service_type: Type):
            """Assert that a service is registered as singleton."""
            binding = self.container._registry.get_binding(service_type)
            if not binding or binding.scope != Scope.SINGLETON:
                raise AssertionError(f"Service {service_type.__name__} is not singleton")

        def assert_transient(self, service_type: Type):
            """Assert that a service is registered as transient."""
            binding = self.container._registry.get_binding(service_type)
            if not binding or binding.scope != Scope.TRANSIENT:
                raise AssertionError(f"Service {service_type.__name__} is not transient")

        def assert_same_instance(self, service_type: Type):
            """Assert that resolving a service returns the same instance."""
            instance1 = self.container.resolve(service_type)
            instance2 = self.container.resolve(service_type)

            if instance1 is not instance2:
                raise AssertionError(f"Service {service_type.__name__} returned different instances")

        def assert_different_instances(self, service_type: Type):
            """Assert that resolving a service returns different instances."""
            instance1 = self.container.resolve(service_type)
            instance2 = self.container.resolve(service_type)

            if instance1 is instance2:
                raise AssertionError(f"Service {service_type.__name__} returned the same instance")

        def assert_mock_called(self, mock: Mock, method_name: str, *args, **kwargs):
            """Assert that a mock method was called with specific arguments."""
            method = getattr(mock, method_name)

            if args or kwargs:
                method.assert_called_with(*args, **kwargs)
            else:
                method.assert_called()

        def assert_mock_not_called(self, mock: Mock, method_name: str):
            """Assert that a mock method was not called."""
            method = getattr(mock, method_name)
            method.assert_not_called()

        def assert_dependency_injected(self, instance: Any, dependency_name: str, expected_type: Type):
            """Assert that a dependency was properly injected."""
            if not hasattr(instance, dependency_name):
                raise AssertionError(f"Instance does not have dependency '{dependency_name}'")

            dependency = getattr(instance, dependency_name)
            if not isinstance(dependency, expected_type):
                raise AssertionError(f"Dependency '{dependency_name}' is not of type {expected_type.__name__}")

    # Usage
    assertions = DIAssertions(container)
    assertions.assert_registered(UserService)
    assertions.assert_singleton(DatabaseConnection)
    assertions.assert_same_instance(CacheService)

### Test Data Builders

    class ServiceBuilder:
        """Builder for creating test service instances."""

        def __init__(self, service_type: Type[T]):
            self.service_type = service_type
            self._dependencies: Dict[str, Any] = {}
            self._properties: Dict[str, Any] = {}

        def with_dependency(self, name: str, value: Any) -> 'ServiceBuilder':
            """Set a dependency value."""
            self._dependencies[name] = value
            return self

        def with_property(self, name: str, value: Any) -> 'ServiceBuilder':
            """Set a property value."""
            self._properties[name] = value
            return self

        def build(self) -> T:
            """Build the service instance."""
            # Create instance with dependencies
            if self._dependencies:
                instance = self.service_type(**self._dependencies)
            else:
                instance = self.service_type()

            # Set properties
            for name, value in self._properties.items():
                setattr(instance, name, value)

            return instance

    class MockBuilder:
        """Builder for creating configured mocks."""

        def __init__(self, service_type: Type):
            self.service_type = service_type
            self._return_values: Dict[str, Any] = {}
            self._side_effects: Dict[str, Any] = {}
            self._properties: Dict[str, Any] = {}

        def returns(self, method_name: str, value: Any) -> 'MockBuilder':
            """Set return value for a method."""
            self._return_values[method_name] = value
            return self

        def raises(self, method_name: str, exception: Exception) -> 'MockBuilder':
            """Set exception to raise for a method."""
            self._side_effects[method_name] = exception
            return self

        def with_property(self, name: str, value: Any) -> 'MockBuilder':
            """Set property value."""
            self._properties[name] = value
            return self

        def build(self) -> Mock:
            """Build the configured mock."""
            mock = Mock(spec=self.service_type)

            # Configure return values
            for method_name, value in self._return_values.items():
                getattr(mock, method_name).return_value = value

            # Configure side effects
            for method_name, effect in self._side_effects.items():
                getattr(mock, method_name).side_effect = effect

            # Configure properties
            for name, value in self._properties.items():
                prop_mock = PropertyMock(return_value=value)
                setattr(type(mock), name, prop_mock)

            return mock

    # Usage
    user_service = (ServiceBuilder(UserService)
        .with_dependency('repository', user_repo_mock)
        .with_dependency('email_service', email_service_mock)
        .with_property('timeout', 30)
        .build())

    email_mock = (MockBuilder(EmailService)
        .returns('send_email', True)
        .raises('send_bulk_email', SMTPException("Server error"))
        .with_property('server_url', "smtp.test.com")
        .build())

## Integration Testing

### Test Harness

    class IntegrationTestHarness:
        """Harness for integration testing with real and mock services."""

        def __init__(self):
            self.container = Container()
            self.test_container = TestContainer(self.container)
            self.real_services: List[Type] = []
            self.mock_services: List[Type] = []

        def use_real_service(self, service_type: Type, implementation: Any = None, scope: str = "transient"):
            """Use real implementation for a service."""
            impl = implementation or service_type
            self.container.register(service_type, impl, scope=scope)
            self.real_services.append(service_type)
            return self

        def use_mock_service(self, service_type: Type, mock_instance: Any = None):
            """Use mock implementation for a service."""
            self.test_container.register_mock(service_type, mock_instance)
            self.mock_services.append(service_type)
            return self

        def configure_database(self, connection_string: str):
            """Configure database for integration tests."""
            # This would set up test database
            self.use_real_service(DatabaseConnection, lambda: create_connection(connection_string))
            return self

        def configure_external_apis(self, mock_responses: Dict[str, Any]):
            """Configure external API mocks."""
            for service_name, responses in mock_responses.items():
                # Create mock with configured responses
                mock = Mock()
                for method, response in responses.items():
                    getattr(mock, method).return_value = response

                # Register mock (would need service type mapping)
                # self.use_mock_service(service_type, mock)

            return self

        def run_integration_test(self, test_func: Callable):
            """Run integration test with configured services."""
            with self.test_container.override_container():
                return self.container.resolve(test_func)

        def cleanup(self):
            """Clean up test resources."""
            # Dispose real services
            for service_type in self.real_services:
                if self.container._instances.get(service_type):
                    instance = self.container._instances[service_type]
                    if hasattr(instance, 'dispose'):
                        instance.dispose()

            # Reset mocks
            self.test_container.reset_mocks()

    # Usage
    harness = IntegrationTestHarness()

    # Configure integration test
    harness.use_real_service(UserRepository, DatabaseUserRepository)
    harness.use_real_service(DatabaseConnection)
    harness.use_mock_service(EmailService)
    harness.use_mock_service(PaymentGateway)

    # Run test
    @inject
    def integration_test(user_service: UserService, email_mock: EmailService):
        # Test with real database but mocked external services
        user = user_service.create_user("test@example.com")
        assert user.id is not None  # Real database assigned ID

        email_mock.send_welcome_email.assert_called_once_with(user.email)

    result = harness.run_integration_test(integration_test)
    harness.cleanup()

### Test Fixtures

    import pytest
    from typing import Generator

    @pytest.fixture
    def test_container() -> Generator[TestContainer, None, None]:
        """Pytest fixture for test container."""
        container = TestContainer()
        yield container
        container.reset_mocks()

    @pytest.fixture
    def user_repository_mock() -> Mock:
        """Pytest fixture for user repository mock."""
        mock = Mock(spec=UserRepository)
        mock.get_user.return_value = User(id=1, email="test@example.com")
        mock.create_user.return_value = User(id=2, email="new@example.com")
        return mock

    @pytest.fixture
    def configured_container(test_container: TestContainer, user_repository_mock: Mock) -> TestContainer:
        """Pytest fixture for configured test container."""
        test_container.register_mock(UserRepository, user_repository_mock)
        test_container.register_mock(EmailService)
        return test_container

    # Test using fixtures
    def test_user_service_creation(configured_container: TestContainer):
        """Test user service with mocked dependencies."""

        @inject
        def test_logic(user_service: UserService) -> User:
            return user_service.create_user("test@example.com")

        result = configured_container.run_test(test_logic)
        assert result.email == "test@example.com"

        # Verify mock interactions
        user_repo_mock = configured_container.get_mock(UserRepository)
        user_repo_mock.create_user.assert_called_once()