Source code for dioxide.testing
"""Testing utilities for dioxide.
This module provides helpers for writing tests with dioxide, making it easy
to create isolated test containers with fresh state.
Instance containers (created via ``Container()`` or ``fresh_container()``) are the
**recommended pattern for testing**. Each container instance has its own singleton
cache, ensuring complete test isolation without state leakage.
For guidance on when to use instance containers vs the global container, see
the Container Patterns guide: :doc:`/docs/user_guide/container_patterns`
Pytest Plugin Usage:
Add the following to your ``conftest.py`` to enable dioxide pytest fixtures::
pytest_plugins = ['dioxide.testing']
This makes the following fixtures available:
- ``dioxide_container``: Fresh container per test (function-scoped)
- ``fresh_container_fixture``: Alias for dioxide_container
- ``dioxide_container_session``: Shared container across tests (session-scoped)
Example using fixtures (recommended)::
# conftest.py
pytest_plugins = ['dioxide.testing']
# test_my_service.py
async def test_something(dioxide_container):
dioxide_container.scan(profile=Profile.TEST)
service = dioxide_container.resolve(MyService)
# ... test with fresh, isolated container
Example using fresh_container context manager::
from dioxide.testing import fresh_container
from dioxide import Profile
async def test_user_registration():
async with fresh_container(profile=Profile.TEST) as container:
service = container.resolve(UserService)
await service.register('alice@example.com', 'Alice')
email = container.resolve(EmailPort)
assert len(email.sent_emails) == 1
"""
from __future__ import annotations
import sys
from collections.abc import (
AsyncIterator,
Iterator,
)
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING
from dioxide.container import Container
if TYPE_CHECKING:
from dioxide.profile_enum import Profile
@asynccontextmanager
[docs]
async def fresh_container(
profile: Profile | str | None = None,
package: str | None = None,
) -> AsyncIterator[Container]:
"""Create a fresh, isolated container for testing.
This context manager creates a new Container instance, scans for components,
manages lifecycle (start/stop), and ensures complete isolation between tests.
This function does NOT require pytest to be installed.
Args:
profile: Profile to scan with (e.g., Profile.TEST). If None, scans all profiles.
package: Optional package to scan. If None, scans all registered components.
Yields:
A fresh Container instance with lifecycle management.
Example:
async with fresh_container(profile=Profile.TEST) as container:
service = container.resolve(UserService)
# ... test with isolated container
# Container automatically cleaned up
"""
container = Container()
container.scan(package=package, profile=profile)
async with container:
yield container
# Pytest fixtures are only defined when pytest is available.
# When pytest_plugins = ['dioxide.testing'] is used, pytest will import this module
# and the fixtures will be available. When pytest is not installed, the fixtures
# simply won't be defined (but fresh_container still works).
# Check if pytest is available by trying to import it
_PYTEST_AVAILABLE = 'pytest' in sys.modules
if not _PYTEST_AVAILABLE:
try:
import pytest as _ # noqa: F401
_PYTEST_AVAILABLE = True
except ImportError:
pass
if _PYTEST_AVAILABLE:
# Import pytest for use in decorators (we know it's available now)
import pytest
@pytest.fixture
[docs]
def dioxide_container() -> Iterator[Container]:
"""Provide a fresh, isolated Container for each test.
This fixture creates a new Container instance for each test function,
ensuring complete isolation between tests. The container is NOT
pre-scanned - you should call ``container.scan(profile=...)`` in your
test to register components with the desired profile.
Yields:
A fresh Container instance.
Example::
async def test_user_service(dioxide_container):
dioxide_container.scan(profile=Profile.TEST)
service = dioxide_container.resolve(UserService)
result = await service.register_user('Alice', 'alice@example.com')
assert result['name'] == 'Alice'
"""
yield Container()
@pytest.fixture
def fresh_container_fixture() -> Iterator[Container]:
"""Alternative fixture with name matching the context manager.
This fixture behaves like ``dioxide_container``, providing a fresh
Container for each test. The name matches the ``fresh_container``
context manager for consistency.
Yields:
A fresh Container instance.
Example::
async def test_isolated(fresh_container_fixture):
fresh_container_fixture.scan(profile=Profile.TEST)
# Guaranteed fresh container, no state leakage
pass
"""
yield Container()
@pytest.fixture(scope='session')
def dioxide_container_session() -> Iterator[Container]:
"""Provide a shared Container for the entire test session.
This session-scoped fixture creates a single Container instance that is
shared across all tests in the session. Use this for performance when
tests can safely share container state.
WARNING: Session-scoped containers share state between tests. Only use
this when you understand the implications and tests are designed to
handle shared state.
Yields:
A shared Container instance for the session.
Example::
# In conftest.py - scan once at session start
@pytest.fixture(scope='session', autouse=True)
def setup_session_container(dioxide_container_session):
dioxide_container_session.scan(profile=Profile.TEST)
# In tests - just use the pre-scanned container
async def test_shared_container(dioxide_container_session):
service = dioxide_container_session.resolve(SharedService)
# ... use shared container
"""
yield Container()