How to mock dependencies? #1789
-
SummaryCan you please add a dependency override to the application. Something similar to fastapi's app.dependency_overrides. Basic Example@pytest.fixture
def client(app):
database = TestAsyncSessionLocal()
def test_get_db(app):
try:
yield database
finally:
database.close()
app.dependency_overrides['db'] = Provide(test_get_db) # nothing like dependency_overrides in starlite
... Drawbacks and ImpactHere's what I tried: def client(app):
database = TestAsyncSessionLocal()
def test_get_db(app):
try:
yield database
finally:
database.close()
app.dependencies['db'] = Provide(test_get_db)
... This doesn't work because handlers are already registered in main:app. So overriding dependencies like this after handlers have been registered changes nothing. Unresolved questionsNo response |
Beta Was this translation helpful? Give feedback.
Replies: 9 comments 3 replies
-
Hi. It's basically already in place. The For controllers you can use attribute mocking. What's missing? |
Beta Was this translation helpful? Give feedback.
-
Thanks for your reply. But the create_test_client was built upon TestClient, and I want to use AsyncTestClient. |
Beta Was this translation helpful? Give feedback.
-
Expanding on what @Goldziher said: It's unlikely that we'll support this for a number of reasons (which we actually cover here in our FastAPI migration guide). The gist is that it's not actually needed and often a crude workaround that has more drawbacks than benefits. Overriding dependencies in such a way:
The solution to your problem is this: @pytest.fixture
def database():
database = TestAsyncSessionLocal()
try:
yield database
finally:
database.close()
@pytest.fixture
def client(app, mocker, database):
mocker.patch("path.to.get_db", return_value=database) This actually has several benefits:
If you do need to actually swap out the whole dependency function because you're performing some additional logic in there you can easily adapt this pattern: def get_database(app):
database = TestAsyncSessionLocal()
try:
yield database
finally:
database.close()
@pytest.fixture
def client(app, mocker, database):
mocker.patch("path.to.get_db", new=get_database) Just to illustrate what the proper way to override dependencies like you want would look like in contrast: # add a separate fixture for the database to ensure it's always closed
# no matter where errors might occur
@pytest.fixture
def database()
database = TestAsyncSessionLocal()
try:
yield database
finally:
if database.is_open:
database.close()
@pytest.fixture
def client(app, database):
def test_get_db(app):
try:
yield database
finally:
database.close()
try:
app.dependency_overrides['db'] = Provide(test_get_db)
finally:
app.dependency_overrides = {} # ensure to reset overrides at the end |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Thanks for your reply. I tried your solution using mock, but it doesn't work. The test still seems to be using my original db instead of the test_db. Here's how my app looks in the main.py:
Please help |
Beta Was this translation helpful? Give feedback.
-
Can you provide an example of your app, the |
Beta Was this translation helpful? Give feedback.
-
app.main.py
app.api.tests.conftest.py
app.core.database.py
Thank you. |
Beta Was this translation helpful? Give feedback.
-
the solution here explains quite well the reasonning, still the test fails for mainly 2 reasons: the
using a context manager to patch should be preferred. see the diff here from a non-passing test to a passing one: euri10/mock_dependency_litestar@9434736 |
Beta Was this translation helpful? Give feedback.
-
The accepted answer has a challenge: you effectively lose the ability to test the actual dependency's internals (problematic if you care about test coverage). I'm running into this with a mocked DB connection—MRE below:
from litestar.datastructures import State
from litestar.di import Provide
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db_session_dependency(state: State) -> AsyncSession:
"""Define a dependency injector to get a DB session.
Args:
----
state: The Litestar app state.
Yields:
------
The session.
"""
sessionmaker = state.db_sessionmaker
raised_exc = None
async with sessionmaker() as session:
try:
yield session
except Exception as exc: # noqa: BLE001
await session.rollback()
raised_exc = exc
finally:
await session.close()
# If we encountered an exception inside the yielded dependency, we raise here to
# ensure it properly propagates up to our handlers:
if raised_exc:
raise raised_exc
DBSessionDependency = Provide(get_db_session_dependency)
"""Define the API."""
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from litestar import Litestar, Router
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from .dependencies import DBSessionDependency
DB_CONNECTION_STRING: Final[str] = "postgresql+psycopg://postgres:postgres@logs-db:5432/my-db"
@asynccontextmanager
async def lifespan_db_sessionmaker(app: Litestar) -> AsyncGenerator[None, None]:
"""Define a lifespan context manager for creating a DB sessionmaker.
Args:
----
app: The Litestar app.
"""
engine = create_async_engine(DB_CONNECTION_STRING)
app.state.db_sessionmaker = async_sessionmaker(autocommit=False, bind=engine)
try:
yield
finally:
await engine.dispose()
def create_app() -> Litestar:
"""Create the app.
Returns
-------
The app.
"""
# Create the API router:
api_router = Router(
path="/api",
route_handlers=[
Router(
path="/v1",
route_handlers=[HEALTH_ROUTER],
)
],
)
# Create the app:
app = Litestar(
dependencies={
"db_session": DBSessionDependency,
},
lifespan=[lifespan_db_sessionmaker],
)
return app
from collections.abc import AsyncGenerator, Generator
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from litestar.testing import AsyncTestClient
import pytest
import pytest_asyncio
@pytest.fixture(name="patch_sqlalchemy")
def _patch_sqlalchemy_fixture(
mock_sqlalchemy_sessionmaker: MagicMock,
) -> Generator[None, None, None]:
"""Define a patch for SQLAlchemy.
Args:
----
mock_sqlalchemy_sessionmaker: The mock SQLAlchemy session maker.
"""
with patch(
"src.app.create_async_engine", return_value=Mock(dispose=AsyncMock())
), patch("src.app.async_sessionmaker", return_value=mock_sqlalchemy_sessionmaker):
yield
@pytest_asyncio.fixture(name="http_client")
async def http_client_fixture(
patch_sqlalchemy: Generator[None, None, None],
) -> AsyncGenerator[AsyncTestClient, None]:
"""Define a test HTTP client.
Args:
----
patch_sqlalchemy: A patch for SQLAlchemy.
Yields:
------
The HTTP client.
"""
app = create_app()
async with AsyncTestClient(app=app) as client:
yield client
@pytest.fixture(name="mock_sqlalchemy_sessionmaker")
def mock_sqlalchemy_sessionmaker_fixture(mock_sqlalchemy_session: Mock) -> MagicMock:
"""Define a mock SQLAlchemy session maker.
Args:
----
mock_sqlalchemy_session: The mock SQLAlchemy session.
Returns:
-------
The mock SQLAlchemy session maker.
"""
return MagicMock(
return_value=MagicMock(
__aenter__=AsyncMock(return_value=mock_sqlalchemy_session),
__aexit__=AsyncMock(),
)
)
@pytest.fixture(name="mock_sqlalchemy_session")
def mock_sqlalchemy_session_fixture(
mock_sqlalchemy_session_commit: AsyncMock,
mock_sqlalchemy_session_execute: AsyncMock,
mock_sqlalchemy_session_rollback: AsyncMock,
) -> Mock:
"""Define a mock SQLAlchemy session.
Args:
----
mock_sqlalchemy_session_commit: The mock SQLAlchemy session commit coroutine.
mock_sqlalchemy_session_execute: The mock SQLAlchemy session execute coroutine.
mock_sqlalchemy_session_rollback: The mock SQLAlchemy session rollback
coroutine.
Returns:
-------
The mock SQLAlchemy session.
"""
return Mock(
add=Mock(),
close=AsyncMock(),
delete=AsyncMock(),
execute=mock_sqlalchemy_session_execute,
commit=mock_sqlalchemy_session_commit,
refresh=AsyncMock(),
rollback=mock_sqlalchemy_session_rollback,
) In production, my dependency returns a
If I merely create a replacement dependency that returns a I know there are ways around this:
...but it would be great if |
Beta Was this translation helpful? Give feedback.
A couple of things here, but let me just say that this isn't something Litestar specific but rather about how scopes, imports and mocking works.
create_app
function and then immediately calling it in the same file is functionally equivalent to simply creating the app in the file directly.app
. This will also set the dependencies on that objectapp.core.database.get_db
. Since the application object has already been created, this will not change theget_db
function you passed to itTo fix it you can either:
get_db
…