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()