Repository navigation
Reconciling pytest fixture injection with app's own service dependency injection system #15076
Replies: 4 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. |
Partial UpdateI can see why this never got "solved" because it has so much app specific AND testing strategy specific sensitivities. Turns out I was using some examples out of #2424 already which is where the A further complication is that I "scan" for the service registrators (functions that register the service factory with the service registry) during application startup, via Another caveat to scanning, is that Some good points were brought up around how the inner service dependencies will be stale if I try to substitute the top level services for any services that are cached. I can't really think of a "this is the right answer" for that. Sometimes just turning off caching might be the safest strategy and then mocking is easy because every test just rebuilds whatever chain it needs but I think all sorts of weird stuff could happen where maybe some things shouldn't be duplicated, or sometimes you WANT to test in the normal way, etc. Also probably slower? Sorry if it seemed like I opened this and went AWOL. I've been meaning to come up with some sort of closure for this but I don't have a cohesive solution yet even for my smaller app/site. I'm slowly updating my test suite so hopefully I can recap eventually with more info. Maybe the best case that could come out of this would just be more how to examples and a little bit of "this is what is expected right now". |
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