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