In a more complicated setup using the python dependency injector framework I use the lifespan function for the FastAPI app object to correctly wire everything.
When testing I'd like to replace some of the objects with different versions (fakes), and the natural way to accomplish that seems to me like I should override or mock the lifespan function of the app object. However I can't seem to figure out if/how I can do that.
MRE follows
import pytest
from contextlib import asynccontextmanager
from fastapi.testclient import TestClient
from fastapi import FastAPI, Response, status
greeting = None
@asynccontextmanager
async def _lifespan(app: FastAPI):
# Initialize dependency injection
global greeting
greeting = "Hello"
yield
@asynccontextmanager
async def _lifespan_override(app: FastAPI):
# Initialize dependency injection
global greeting
greeting = "Hi"
yield
app = FastAPI(title="Test", lifespan=_lifespan)
@app.get("/")
async def root():
return Response(status_code=status.HTTP_200_OK, content=greeting)
@pytest.fixture
def fake_client():
with TestClient(app) as client:
yield client
def test_override(fake_client):
response = fake_client.get("/")
assert response.text == "Hi"
So basically in the fake_client fixture I'd like to change it to use the _lifespan_override instead of the original _lifespan, making the dummy test-case above pass
I'd have expected something like with TestClient(app, lifespan=_lifespan_override) as client: to work, but that's not supported. Is there some way I can mock it to get the behavior I want?
(The mre above works if you replace "Hi" with "Hello" in the assert statement)
pyproject.toml below with needed dependencies
[tool.poetry]
name = "mre"
version = "0.1.0"
description = "mre"
authors = []
[tool.poetry.dependencies]
python = "^3.10"
fastapi = "^0.103.2"
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.2"
httpx = "^0.25.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
EDIT: Tried extending my code with the suggestion from Hamed Akhavan below as follows
@pytest.fixture
def fake_client():
app.dependency_overrides[_lifespan] = _lifespan_override
with TestClient(app) as client:
yield client
but it doesn't work, even though it looks like it should be the right approach. Syntax problem?
You can do it like this:
from starlette.routing import _DefaultLifespan
@pytest.fixture
def client():
# import app here
# mock lifespan
app.router.lifespan_context = _DefaultLifespan(app.router)
with TestClient(app) as client:
yield client
I found a solution to my problem that didn't include overriding the lifespan function, so not a general solution to my questions above.
As I mentioned my specific problem in the real application was using the python dependency injector framework, and it provides and override method for it's containers. So the solution was to use that override functionality when wiring the dependencies during testing, which means the lifespan function doesn't need to be touched
Here's a complete working MRE in case anyone is interested.
import pytest
from contextlib import asynccontextmanager
from fastapi.testclient import TestClient
from fastapi import FastAPI, Response, status, Depends
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
class HelloGreeter():
def greet(self):
return "Hello"
class Container(containers.DeclarativeContainer):
greeter = providers.Singleton(HelloGreeter)
@asynccontextmanager
async def _lifespan(app: FastAPI):
# Initialize dependency injection
container = Container()
container.wire(modules=[__name__])
yield
app = FastAPI(title="Test", lifespan=_lifespan)
@app.get("/")
@inject
async def root(greeter=Depends(Provide[Container.greeter])):
return Response(status_code=status.HTTP_200_OK, content=greeter.greet())
@pytest.fixture
def fake_client():
class HiGreeter():
def greet(self):
return "Hi"
with Container.greeter.override(HiGreeter()):
with TestClient(app) as client:
yield client
def test_override(fake_client):
response = fake_client.get("/")
assert response.text == "Hi"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With