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

Skip to content

# Testing Patterns

Testing is critical for dependency-injected code. InjectQ provides utilities to isolate dependencies, mock services, and verify behavior.

## Core Testing Utilities

    from injectq.testing import (
        test_container,           # Create isolated test containers
        override_dependency,      # Temporarily override a binding
        mock_factory,            # Create mock factory functions
        pytest_container_fixture, # Pytest fixture for test containers
    )

## Basic Testing Pattern

### Using test_container()

    from injectq import InjectQ, inject
    from injectq.testing import test_container

    # Create an isolated container for each test
    def test_user_service():
        with test_container() as container:
            # Bind dependencies
            container[str] = "test-database-url"
            container[UserService] = UserService

            # Get service - automatically resolves dependencies
            service = container[UserService]

            # Test the service
            result = service.get_user(1)
            assert result is not None

### Using override_dependency()

    from injectq import InjectQ
    from injectq.testing import override_dependency

    def test_with_mocked_dependency():
        container = InjectQ.get_instance()

        # Temporarily replace a service with a mock
        mock_db = MockDatabase()

        with override_dependency(Database, mock_db):
            service = container[UserService]
            result = service.get_user(1)

            # Verify mock was used
            assert mock_db.query_called is True

## Unit Testing

Test services in isolation by mocking their dependencies:

    from injectq.testing import test_container

    def test_user_service_create_user():
        """Test UserService.create_user() with mock database."""
        with test_container() as container:
            # Create mock repository
            mock_repo = MockUserRepository()

            # Bind mock into container
            container[UserRepository] = mock_repo
            container[UserService] = UserService

            # Get service and test
            service = container[UserService]
            user = service.create_user("john@example.com")

            # Verify behavior
            assert user.email == "john@example.com"
            assert mock_repo.save_called is True

## Mock Implementation Example

    class MockUserRepository:
        """Mock repository for testing."""

        def __init__(self):
            self.users = {}
            self.save_called = False
            self.get_called = False

        def save(self, user):
            self.save_called = True
            self.users[user.id] = user
            return user

        def get_by_id(self, user_id):
            self.get_called = True
            return self.users.get(user_id)

        def get_by_email(self, email):
            for user in self.users.values():
                if user.email == email:
                    return user
            return None

## Mock Factory Pattern

Use `mock_factory` for factory-based dependencies:

    from injectq.testing import mock_factory

    def test_with_factory():
        """Test using mock factories."""
        with test_container() as container:
            # Bind a mock factory
            container.bind_factory(
                "connection_id",
                mock_factory(lambda: "mock-connection-123")
            )

            # Service gets mocked value
            service = container[ServiceUsingConnectionId]
            result = service.do_work()

            assert result is not None

### Testing Parameterized Factories

Test factories that accept arguments:

    from injectq.testing import test_container

    def test_parameterized_factory():
        """Test parameterized factory with different arguments."""
        with test_container() as container:
            # Bind a parameterized factory
            def create_pool(db_name: str, max_conn: int = 10):
                return ConnectionPool(db_name, max_conn=max_conn)

            container.bind_factory("pool", create_pool)

            # Test with different parameters
            users_pool = container.call_factory("pool", "users_db", max_conn=20)
            orders_pool = container.call_factory("pool", "orders_db", max_conn=15)

            # Verify each has correct parameters
            assert users_pool.db_name == "users_db"
            assert users_pool.max_connections == 20

            assert orders_pool.db_name == "orders_db"
            assert orders_pool.max_connections == 15

            # Verify they are different instances
            assert users_pool is not orders_pool

    def test_factory_with_mock_dependencies():
        """Test parameterized factory that uses DI."""
        with test_container() as container:
            # Mock a dependency
            mock_db = MockDatabase()
            container[Database] = mock_db

            # Parameterized factory that uses DI
            def get_user(user_id: int):
                db = container[Database]
                return db.get_user(user_id)

            container.bind_factory("get_user", get_user)

            # Mock the database response
            mock_db.users = {1: {"id": 1, "name": "Alice"}}

            # Test with parameter
            user = container.call_factory("get_user", 1)
            assert user["name"] == "Alice"

## Pytest Integration

Use pytest fixtures for convenient test setup:

    import pytest
    from injectq.testing import pytest_container_fixture

    # Create a pytest fixture
    container = pytest_container_fixture()

    def test_with_fixture(container):
        """Test using pytest fixture."""
        # container is a fresh InjectQ instance for each test
        container[UserService] = UserService
        container[str] = "test-config"

        service = container[UserService]
        assert service is not None

    # Or create a custom fixture
    @pytest.fixture
    def test_app_container():
        from injectq.testing import test_container
        with test_container() as container:
            # Setup common test bindings
            container[str] = "test-database-url"
            yield container

    def test_with_custom_fixture(test_app_container):
        test_app_container[UserService] = UserService
        service = test_app_container[UserService]
        assert service is not None

## Testing Scopes

Test scoped services:

    from injectq import singleton, scoped, transient

    def test_singleton_scope():
        """Verify singleton scope creates one instance."""
        with test_container() as container:
            @singleton
            class SingletonService:
                pass

            container[SingletonService] = SingletonService

            # Same instance every time
            instance1 = container[SingletonService]
            instance2 = container[SingletonService]

            assert instance1 is instance2

    def test_transient_scope():
        """Verify transient scope creates new instances."""
        with test_container() as container:
            @transient
            class TransientService:
                pass

            container[TransientService] = TransientService

            # Different instance every time
            instance1 = container[TransientService]
            instance2 = container[TransientService]

            assert instance1 is not instance2

## Testing with Real vs Mock Dependencies

    def test_mixed_real_and_mocked():
        """Use real services where possible, mock only external dependencies."""
        with test_container() as container:
            # Real internal services
            container[UserService] = UserService
            container[UserValidator] = UserValidator

            # Mock external services
            container[EmailService] = MockEmailService()
            container[PaymentService] = MockPaymentService()

            service = container[UserService]
            result = service.register_user("john@example.com", "password")

            # Verify result
            assert result.email == "john@example.com"

            # Verify external services were called correctly
            email_service = container[EmailService]
            assert email_service.confirmation_email_sent

## Error Testing

    import pytest

    def test_error_handling():
        """Test service error handling."""
        with test_container() as container:
            container[UserService] = UserService
            container[UserRepository] = MockUserRepository()

            service = container[UserService]

            # Test that errors are raised correctly
            with pytest.raises(UserNotFoundError):
                service.get_user(999)

## Best Practices

### ✅ DO

- Use test_container() for isolated test environments
- Mock external dependencies (APIs, databases, email services)
- Keep tests focused on one behavior per test
- Use fixtures for common setup
- Name mocks clearly to distinguish from real services

    # ✅ Good
    def test_user_creation():
        with test_container() as container:
            mock_repo = MockUserRepository()
            container[UserRepository] = mock_repo
            container[UserService] = UserService

            service = container[UserService]
            user = service.create_user("test@example.com")

            assert mock_repo.save_called

### ❌ DON'T

- Don't use the global container in tests without override_dependency
- Don't over-mock - only mock external dependencies
- Don't test private methods - test public interfaces
- Don't share state between tests

    # ❌ Bad
    def test_user_creation():
        # Uses global container - tests interfere with each other
        container = InjectQ.get_instance()
        service = container[UserService]
        # ...

## Common Testing Patterns

### Pattern 1: Verify Method Calls

    class TrackingMockService:
        def __init__(self):
            self.call_log = []

        def some_method(self, arg):
            self.call_log.append(("some_method", arg))
            return f"result_{arg}"

    def test_service_calls_dependency():
        with test_container() as container:
            tracking_mock = TrackingMockService()
            container[DependentService] = tracking_mock

            service = container[MainService]
            service.do_work("test")

            # Verify the dependency was called
            assert ("some_method", "test") in tracking_mock.call_log

### Pattern 2: Control Return Values

    class ConfigurableMockService:
        def __init__(self):
            self.return_values = {}

        def set_return_value(self, method, value):
            self.return_values[method] = value

        def get_data(self):
            return self.return_values.get("get_data", None)

    def test_with_specific_return_values():
        with test_container() as container:
            mock_service = ConfigurableMockService()
            mock_service.set_return_value("get_data", {"id": 1, "name": "Test"})

            container[DataService] = mock_service
            container[ConsumerService] = ConsumerService

            service = container[ConsumerService]
            result = service.process()

            assert result["id"] == 1

### Pattern 3: Temporary Override

    def test_with_temporary_override():
        """Override a dependency for a specific test."""
        container = InjectQ.get_instance()

        # Original binding
        container[ConfigService] = RealConfigService()

        real_service = container[ConfigService]
        assert isinstance(real_service, RealConfigService)

        # Temporarily override
        mock_config = MockConfigService()
        with override_dependency(ConfigService, mock_config):
            service = container[ConfigService]
            assert isinstance(service, MockConfigService)

            # Do test assertions here

        # Override ends - back to real service
        service = container[ConfigService]
        assert isinstance(service, RealConfigService)

## Running Tests with Pytest

    # Run all tests
    pytest

    # Run specific test file
    pytest tests/test_services.py

    # Run with coverage
    pytest --cov=src

    # Show coverage report
    pytest --cov=src --cov-report=html

## Summary

Key testing practices with InjectQ:

- Use `test_container()` for isolated test environments
- Use `override_dependency()` for temporary service replacement
- Mock external dependencies, use real internal services
- Test behavior, not implementation details
- Keep tests fast and independent
- Use pytest fixtures for common setup