Repository navigation
Reconciling pytest fixture injection with app's own service dependency injection system #15076
Replies: 3 comments 1 reply
|
Option 1 is actually a legitimate, well-behaved way to do this — pytest discovers fixtures by scanning the conftest module's attributes, so it doesn't care whether a fixture was written by hand or assigned in a loop. The trick is to generate them through a small factory and give each one an explicit # conftest.py
import pytest
from app.container import build_container
from app.protocols import IServiceA, IServiceB # ...
# fixture name -> interface; could also be derived from the registry itself
SERVICES = {
"service_a": IServiceA,
"service_b": IServiceB,
}
@pytest.fixture
def app_container():
container = build_container() # however the app builds it
yield container
container.close() # if it has teardown
def _make_service_fixture(name, iface):
@pytest.fixture(name=name)
def _fixture(app_container):
return app_container.get(iface)
_fixture.__doc__ = f"{iface.__name__} resolved from the app container."
return _fixture
for _name, _iface in SERVICES.items():
globals()[_name] = _make_service_fixture(_name, _iface)Tests then look exactly like your Option 0 example — A few things that make this nicer in practice:
I'd avoid Option 2 (wrapping tests / |
|
The idiomatic pattern is generating fixtures in # conftest.py
import pytest
from app.registry import services # your container
def _make_fixture(svc_name, scope="function"):
@pytest.fixture(name=svc_name, scope=scope)
def _f(request):
return services.resolve(svc_name)
return _f
for name in services.registered():
globals()[f"svc_{name}"] = _make_fixture(name)Pass |
|
Your option 0 is a reasonable default, especially if only a subset of the registered services is used directly by tests. Each fixture is a small adapter; the app container still resolves the service's own dependencies. If the repetition is the main concern, another option is a single resolver fixture: @pytest.fixture
def resolve_service(app_container):
return app_container.get
def test_abc(resolve_service, fixture_b):
service_a = resolve_service(IServiceA)
assert service_a.add_to_a(fixture_b, "c") == "abc"This keeps the container responsible for its dependency graph and caching, while pytest owns the container fixture's lifetime. It avoids generating 100 fixture names, at the cost of losing One caveat with either explicit or generated adapters: overriding a pytest fixture named Likewise, use a request/child scope per test when that matches the app's lifecycle, and finalize it in the container fixture. A session-wide cache can otherwise retain dependencies across tests. I checked the resolver/cache identity and the fixture-override distinction with a small executable example on Python 3.12.10 / pytest 9.1.1. This does not test your specific DI library. Reference: pytest documents returning callables in factories as fixtures. Prepared with AI assistance; the example behavior was checked locally. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
I have a web app that has a service registry/container that maps an interface to factory that is cached after construction, depending on the scope. In a way it is similar to pytest fixtures themselves. When I'm writing tests I want to load those services alongside fixtures. Or really just "auto" make the fixtures.
What is a good way to do that? I think maybe just hard coding all the fixtures is the easiest way (after writing this up). It always seems like it would be repetitive but it would probably be write once use until the actual service(s) changed.
Some options I've tried:
globals()via some module level code inconftest.pyglobals()[service_fixture_name] = service_fixture_funcinconftest.pypytest_generate_teststo parameterize in the app service fixtures?Option 0
Edited: Try to fix formatting.
All reactions