Fixtures for testing¶
The recommended way to test modern-di applications with pytest is to
install modern-di-pytest, which exposes any DI
dependency as a pytest fixture. The plain-modern-di recipe at the bottom of
this page still works and remains the right choice when you cannot add a
test-only dependency.
1. Recommended: modern-di-pytest¶
# conftest.py
import typing
import modern_di
import pytest
from modern_di_pytest import expose, modern_di_fixture
from app import ioc
from app.ioc import Dependencies
from app.services import EmailClient
@pytest.fixture(scope="session")
def di_container() -> typing.Iterator[modern_di.Container]:
with modern_di.Container(groups=ioc.ALL_GROUPS) as container:
yield container
@pytest.fixture
def di_request_container(
di_container: modern_di.Container,
) -> typing.Iterator[modern_di.Container]:
with di_container.build_child_container(scope=modern_di.Scope.REQUEST) as container:
yield container
# Bulk: every Provider on each group becomes a pytest fixture named after
# the class attribute. Pass several groups in one call if you split your
# providers across multiple Group subclasses.
expose(Dependencies)
# Manual: a single dependency as a named fixture.
email_client = modern_di_fixture(EmailClient)
Tests then receive resolved dependencies by name:
from app.services import EmailClient, SimpleFactory
def test_with_app_scope(simple_factory: SimpleFactory) -> None:
# `simple_factory` was generated by expose(Dependencies)
...
def test_email(email_client: EmailClient) -> None:
email_client.send("hi")
For overrides, use Container.override() directly — see the pytest
integration page for the full pattern.
!!! note "Overrides are global"
container.override() and container.reset_override() operate on the
shared overrides registry, which is shared across all containers in the
same tree (parent and all children). Calling override() on a child
container affects every container in the tree for the duration of the
override. Always call reset_override() in a finally block or use a
fixture that guarantees cleanup.
2. Without modern-di-pytest — plain modern-di recipe¶
If you cannot add modern-di-pytest as a dev dependency, write the
fixtures by hand. The example below is the equivalent of what
modern-di-pytest would generate:
import typing
import modern_di
import modern_di_fastapi
import pytest
from fastapi import FastAPI
from app import ioc
from app.ioc import Dependencies, SimpleFactory
application = FastAPI()
modern_di_fastapi.setup_di(application, modern_di.Container(groups=ioc.ALL_GROUPS))
@pytest.fixture
async def di_container() -> typing.AsyncIterator[modern_di.Container]:
async with modern_di_fastapi.fetch_di_container(application) as container:
yield container
@pytest.fixture
async def request_di_container(
di_container: modern_di.Container,
) -> typing.AsyncIterator[modern_di.Container]:
async with di_container.build_child_container(scope=modern_di.Scope.REQUEST) as container:
yield container
@pytest.fixture
def mock_dependencies(di_container: modern_di.Container) -> typing.Iterator[None]:
di_container.override(
provider=Dependencies.simple_factory,
override_object=SimpleFactory(dep1="mock", dep2=777),
)
yield
di_container.reset_override(Dependencies.simple_factory)
Use the fixtures in tests:
import pytest
from app.ioc import Dependencies
def test_with_app_scope(di_container: modern_di.Container) -> None:
resource_instance = di_container.resolve_provider(Dependencies.sync_resource)
# Do something with the dependency
def test_with_request_scope(request_di_container: modern_di.Container) -> None:
simple_factory_instance = request_di_container.resolve_provider(Dependencies.simple_factory)
# Do something with the dependency
@pytest.mark.usefixtures("mock_dependencies")
def test_with_request_scope_mocked(request_di_container: modern_di.Container) -> None:
simple_factory_instance = request_di_container.resolve_provider(Dependencies.simple_factory)
# The dependency is mocked here